Add environment management for projects with variable support
This commit is contained in:
parent
26970cc392
commit
24b1db4b9b
137
src/environments-dialog.ui
Normal file
137
src/environments-dialog.ui
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk" version="4.0"/>
|
||||||
|
<requires lib="libadwaita" version="1.0"/>
|
||||||
|
|
||||||
|
<template class="EnvironmentsDialog" parent="AdwDialog">
|
||||||
|
<property name="title">Manage Environments</property>
|
||||||
|
<property name="content-width">700</property>
|
||||||
|
<property name="content-height">500</property>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="AdwToolbarView">
|
||||||
|
<child type="top">
|
||||||
|
<object class="AdwHeaderBar">
|
||||||
|
<property name="show-title">true</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<property name="content">
|
||||||
|
<object class="GtkScrolledWindow">
|
||||||
|
<property name="vexpand">true</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="margin-start">12</property>
|
||||||
|
<property name="margin-end">12</property>
|
||||||
|
<property name="margin-top">12</property>
|
||||||
|
<property name="margin-bottom">12</property>
|
||||||
|
<property name="spacing">24</property>
|
||||||
|
|
||||||
|
<!-- Variables Section -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">12</property>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">12</property>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="label">Variables</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="title-4"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="add_variable_button">
|
||||||
|
<property name="icon-name">list-add-symbolic</property>
|
||||||
|
<property name="tooltip-text">Add variable</property>
|
||||||
|
<signal name="clicked" handler="on_add_variable_clicked"/>
|
||||||
|
<style>
|
||||||
|
<class name="flat"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="variables_listbox">
|
||||||
|
<property name="selection-mode">none</property>
|
||||||
|
<style>
|
||||||
|
<class name="boxed-list"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<!-- Environments Section -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">12</property>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">12</property>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="label">Environments</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="title-4"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="add_environment_button">
|
||||||
|
<property name="icon-name">list-add-symbolic</property>
|
||||||
|
<property name="tooltip-text">Add environment</property>
|
||||||
|
<signal name="clicked" handler="on_add_environment_clicked"/>
|
||||||
|
<style>
|
||||||
|
<class name="flat"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkFrame">
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="environments_listbox">
|
||||||
|
<property name="selection-mode">none</property>
|
||||||
|
<style>
|
||||||
|
<class name="boxed-list"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</template>
|
||||||
|
</interface>
|
||||||
233
src/environments_dialog.py
Normal file
233
src/environments_dialog.py
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
# environments_dialog.py
|
||||||
|
#
|
||||||
|
# Copyright 2025 Pavel Baksy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from gi.repository import Adw, Gtk, GObject
|
||||||
|
from .widgets.variable_row import VariableRow
|
||||||
|
from .widgets.environment_row import EnvironmentRow
|
||||||
|
|
||||||
|
|
||||||
|
@Gtk.Template(resource_path='/cz/vesp/roster/environments-dialog.ui')
|
||||||
|
class EnvironmentsDialog(Adw.Dialog):
|
||||||
|
"""Dialog for managing project environments and variables."""
|
||||||
|
|
||||||
|
__gtype_name__ = 'EnvironmentsDialog'
|
||||||
|
|
||||||
|
variables_listbox = Gtk.Template.Child()
|
||||||
|
add_variable_button = Gtk.Template.Child()
|
||||||
|
environments_listbox = Gtk.Template.Child()
|
||||||
|
add_environment_button = Gtk.Template.Child()
|
||||||
|
|
||||||
|
__gsignals__ = {
|
||||||
|
'environments-updated': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, project, project_manager):
|
||||||
|
super().__init__()
|
||||||
|
self.project = project
|
||||||
|
self.project_manager = project_manager
|
||||||
|
self._populate_variables()
|
||||||
|
self._populate_environments()
|
||||||
|
|
||||||
|
def _populate_variables(self):
|
||||||
|
"""Populate variables list."""
|
||||||
|
# Clear existing
|
||||||
|
while child := self.variables_listbox.get_first_child():
|
||||||
|
self.variables_listbox.remove(child)
|
||||||
|
|
||||||
|
# Add variables
|
||||||
|
for var_name in self.project.variable_names:
|
||||||
|
row = VariableRow(var_name)
|
||||||
|
row.connect('remove-requested', self._on_variable_remove, var_name)
|
||||||
|
row.connect('changed', self._on_variable_changed, var_name, row)
|
||||||
|
self.variables_listbox.append(row)
|
||||||
|
|
||||||
|
def _populate_environments(self):
|
||||||
|
"""Populate environments list."""
|
||||||
|
# Clear existing
|
||||||
|
while child := self.environments_listbox.get_first_child():
|
||||||
|
self.environments_listbox.remove(child)
|
||||||
|
|
||||||
|
# Add environments
|
||||||
|
for env in self.project.environments:
|
||||||
|
row = EnvironmentRow(env, self.project.variable_names)
|
||||||
|
row.connect('edit-requested', self._on_environment_edit, env)
|
||||||
|
row.connect('delete-requested', self._on_environment_delete, env)
|
||||||
|
row.connect('value-changed', self._on_environment_value_changed, env)
|
||||||
|
self.environments_listbox.append(row)
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def on_add_variable_clicked(self, button):
|
||||||
|
"""Add new variable."""
|
||||||
|
dialog = Adw.AlertDialog()
|
||||||
|
dialog.set_heading("New Variable")
|
||||||
|
dialog.set_body("Enter variable name:")
|
||||||
|
|
||||||
|
entry = Gtk.Entry()
|
||||||
|
entry.set_placeholder_text("Variable name")
|
||||||
|
dialog.set_extra_child(entry)
|
||||||
|
|
||||||
|
dialog.add_response("cancel", "Cancel")
|
||||||
|
dialog.add_response("add", "Add")
|
||||||
|
dialog.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED)
|
||||||
|
dialog.set_default_response("add")
|
||||||
|
dialog.set_close_response("cancel")
|
||||||
|
|
||||||
|
def on_response(dlg, response):
|
||||||
|
if response == "add":
|
||||||
|
name = entry.get_text().strip()
|
||||||
|
if name and name not in self.project.variable_names:
|
||||||
|
self.project_manager.add_variable(self.project.id, name)
|
||||||
|
# Reload project data
|
||||||
|
self._reload_project()
|
||||||
|
self._populate_variables()
|
||||||
|
self._populate_environments()
|
||||||
|
self.emit('environments-updated')
|
||||||
|
|
||||||
|
dialog.connect("response", on_response)
|
||||||
|
dialog.present(self)
|
||||||
|
|
||||||
|
def _on_variable_remove(self, widget, var_name):
|
||||||
|
"""Remove variable with confirmation."""
|
||||||
|
dialog = Adw.AlertDialog()
|
||||||
|
dialog.set_heading("Delete Variable?")
|
||||||
|
dialog.set_body(f"Delete variable '{var_name}' from all environments? This cannot be undone.")
|
||||||
|
|
||||||
|
dialog.add_response("cancel", "Cancel")
|
||||||
|
dialog.add_response("delete", "Delete")
|
||||||
|
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
|
||||||
|
dialog.set_close_response("cancel")
|
||||||
|
|
||||||
|
def on_response(dlg, response):
|
||||||
|
if response == "delete":
|
||||||
|
self.project_manager.delete_variable(self.project.id, var_name)
|
||||||
|
self._reload_project()
|
||||||
|
self._populate_variables()
|
||||||
|
self._populate_environments()
|
||||||
|
self.emit('environments-updated')
|
||||||
|
|
||||||
|
dialog.connect("response", on_response)
|
||||||
|
dialog.present(self)
|
||||||
|
|
||||||
|
def _on_variable_changed(self, widget, old_name, row):
|
||||||
|
"""Handle variable name change."""
|
||||||
|
new_name = row.get_variable_name()
|
||||||
|
if new_name and new_name != old_name and new_name not in self.project.variable_names:
|
||||||
|
self.project_manager.rename_variable(self.project.id, old_name, new_name)
|
||||||
|
self._reload_project()
|
||||||
|
self._populate_variables()
|
||||||
|
self._populate_environments()
|
||||||
|
self.emit('environments-updated')
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def on_add_environment_clicked(self, button):
|
||||||
|
"""Add new environment."""
|
||||||
|
dialog = Adw.AlertDialog()
|
||||||
|
dialog.set_heading("New Environment")
|
||||||
|
dialog.set_body("Enter environment name:")
|
||||||
|
|
||||||
|
entry = Gtk.Entry()
|
||||||
|
entry.set_placeholder_text("Environment name (e.g., Production)")
|
||||||
|
dialog.set_extra_child(entry)
|
||||||
|
|
||||||
|
dialog.add_response("cancel", "Cancel")
|
||||||
|
dialog.add_response("add", "Add")
|
||||||
|
dialog.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED)
|
||||||
|
dialog.set_default_response("add")
|
||||||
|
dialog.set_close_response("cancel")
|
||||||
|
|
||||||
|
def on_response(dlg, response):
|
||||||
|
if response == "add":
|
||||||
|
name = entry.get_text().strip()
|
||||||
|
if name:
|
||||||
|
self.project_manager.add_environment(self.project.id, name)
|
||||||
|
self._reload_project()
|
||||||
|
self._populate_environments()
|
||||||
|
self.emit('environments-updated')
|
||||||
|
|
||||||
|
dialog.connect("response", on_response)
|
||||||
|
dialog.present(self)
|
||||||
|
|
||||||
|
def _on_environment_edit(self, widget, environment):
|
||||||
|
"""Edit environment name."""
|
||||||
|
dialog = Adw.AlertDialog()
|
||||||
|
dialog.set_heading("Edit Environment")
|
||||||
|
dialog.set_body("Enter new name:")
|
||||||
|
|
||||||
|
entry = Gtk.Entry()
|
||||||
|
entry.set_text(environment.name)
|
||||||
|
dialog.set_extra_child(entry)
|
||||||
|
|
||||||
|
dialog.add_response("cancel", "Cancel")
|
||||||
|
dialog.add_response("save", "Save")
|
||||||
|
dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED)
|
||||||
|
|
||||||
|
def on_response(dlg, response):
|
||||||
|
if response == "save":
|
||||||
|
new_name = entry.get_text().strip()
|
||||||
|
if new_name:
|
||||||
|
self.project_manager.update_environment(self.project.id, environment.id, name=new_name)
|
||||||
|
self._reload_project()
|
||||||
|
self._populate_environments()
|
||||||
|
self.emit('environments-updated')
|
||||||
|
|
||||||
|
dialog.connect("response", on_response)
|
||||||
|
dialog.present(self)
|
||||||
|
|
||||||
|
def _on_environment_delete(self, widget, environment):
|
||||||
|
"""Delete environment with confirmation."""
|
||||||
|
# Prevent deletion of last environment
|
||||||
|
if len(self.project.environments) <= 1:
|
||||||
|
dialog = Adw.AlertDialog()
|
||||||
|
dialog.set_heading("Cannot Delete")
|
||||||
|
dialog.set_body("Each project must have at least one environment.")
|
||||||
|
dialog.add_response("ok", "OK")
|
||||||
|
dialog.present(self)
|
||||||
|
return
|
||||||
|
|
||||||
|
dialog = Adw.AlertDialog()
|
||||||
|
dialog.set_heading("Delete Environment?")
|
||||||
|
dialog.set_body(f"Delete environment '{environment.name}'? This cannot be undone.")
|
||||||
|
|
||||||
|
dialog.add_response("cancel", "Cancel")
|
||||||
|
dialog.add_response("delete", "Delete")
|
||||||
|
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
|
||||||
|
dialog.set_close_response("cancel")
|
||||||
|
|
||||||
|
def on_response(dlg, response):
|
||||||
|
if response == "delete":
|
||||||
|
self.project_manager.delete_environment(self.project.id, environment.id)
|
||||||
|
self._reload_project()
|
||||||
|
self._populate_environments()
|
||||||
|
self.emit('environments-updated')
|
||||||
|
|
||||||
|
dialog.connect("response", on_response)
|
||||||
|
dialog.present(self)
|
||||||
|
|
||||||
|
def _on_environment_value_changed(self, widget, var_name, value, environment):
|
||||||
|
"""Handle environment variable value change."""
|
||||||
|
self.project_manager.update_environment_variable(self.project.id, environment.id, var_name, value)
|
||||||
|
self.emit('environments-updated')
|
||||||
|
|
||||||
|
def _reload_project(self):
|
||||||
|
"""Reload project data from manager."""
|
||||||
|
projects = self.project_manager.load_projects()
|
||||||
|
for p in projects:
|
||||||
|
if p.id == self.project.id:
|
||||||
|
self.project = p
|
||||||
|
break
|
||||||
@ -37,6 +37,7 @@ roster_sources = [
|
|||||||
'tab_manager.py',
|
'tab_manager.py',
|
||||||
'constants.py',
|
'constants.py',
|
||||||
'icon_picker_dialog.py',
|
'icon_picker_dialog.py',
|
||||||
|
'environments_dialog.py',
|
||||||
'preferences_dialog.py',
|
'preferences_dialog.py',
|
||||||
'request_tab_widget.py',
|
'request_tab_widget.py',
|
||||||
]
|
]
|
||||||
@ -50,6 +51,8 @@ widgets_sources = [
|
|||||||
'widgets/history_item.py',
|
'widgets/history_item.py',
|
||||||
'widgets/project_item.py',
|
'widgets/project_item.py',
|
||||||
'widgets/request_item.py',
|
'widgets/request_item.py',
|
||||||
|
'widgets/variable_row.py',
|
||||||
|
'widgets/environment_row.py',
|
||||||
]
|
]
|
||||||
|
|
||||||
install_data(widgets_sources, install_dir: moduledir / 'widgets')
|
install_data(widgets_sources, install_dir: moduledir / 'widgets')
|
||||||
|
|||||||
@ -121,14 +121,51 @@ class SavedRequest:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Environment:
|
||||||
|
"""An environment with variable values."""
|
||||||
|
id: str # UUID
|
||||||
|
name: str # e.g., "production", "integration", "stage"
|
||||||
|
variables: Dict[str, str] # variable_name -> value
|
||||||
|
created_at: str # ISO format
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert to dictionary for JSON serialization."""
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'variables': self.variables,
|
||||||
|
'created_at': self.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data):
|
||||||
|
"""Create instance from dictionary."""
|
||||||
|
return cls(
|
||||||
|
id=data['id'],
|
||||||
|
name=data['name'],
|
||||||
|
variables=data.get('variables', {}),
|
||||||
|
created_at=data['created_at']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Project:
|
class Project:
|
||||||
"""A project containing saved requests."""
|
"""A project containing saved requests and environments."""
|
||||||
id: str # UUID
|
id: str # UUID
|
||||||
name: str
|
name: str
|
||||||
requests: List[SavedRequest]
|
requests: List[SavedRequest]
|
||||||
created_at: str # ISO format
|
created_at: str # ISO format
|
||||||
icon: str = "folder-symbolic" # Icon name
|
icon: str = "folder-symbolic" # Icon name
|
||||||
|
variable_names: List[str] = None # List of variable names defined for this project
|
||||||
|
environments: List[Environment] = None # List of environments
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
"""Initialize optional fields."""
|
||||||
|
if self.variable_names is None:
|
||||||
|
self.variable_names = []
|
||||||
|
if self.environments is None:
|
||||||
|
self.environments = []
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""Convert to dictionary for JSON serialization."""
|
"""Convert to dictionary for JSON serialization."""
|
||||||
@ -137,7 +174,9 @@ class Project:
|
|||||||
'name': self.name,
|
'name': self.name,
|
||||||
'requests': [req.to_dict() for req in self.requests],
|
'requests': [req.to_dict() for req in self.requests],
|
||||||
'created_at': self.created_at,
|
'created_at': self.created_at,
|
||||||
'icon': self.icon
|
'icon': self.icon,
|
||||||
|
'variable_names': self.variable_names,
|
||||||
|
'environments': [env.to_dict() for env in self.environments]
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -148,7 +187,9 @@ class Project:
|
|||||||
name=data['name'],
|
name=data['name'],
|
||||||
requests=[SavedRequest.from_dict(r) for r in data.get('requests', [])],
|
requests=[SavedRequest.from_dict(r) for r in data.get('requests', [])],
|
||||||
created_at=data['created_at'],
|
created_at=data['created_at'],
|
||||||
icon=data.get('icon', 'folder-symbolic') # Default for old data
|
icon=data.get('icon', 'folder-symbolic'), # Default for old data
|
||||||
|
variable_names=data.get('variable_names', []),
|
||||||
|
environments=[Environment.from_dict(e) for e in data.get('environments', [])]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ from datetime import datetime, timezone
|
|||||||
import gi
|
import gi
|
||||||
gi.require_version('GLib', '2.0')
|
gi.require_version('GLib', '2.0')
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
from .models import Project, SavedRequest, HttpRequest
|
from .models import Project, SavedRequest, HttpRequest, Environment
|
||||||
|
|
||||||
|
|
||||||
class ProjectManager:
|
class ProjectManager:
|
||||||
@ -69,13 +69,25 @@ class ProjectManager:
|
|||||||
print(f"Error saving projects: {e}")
|
print(f"Error saving projects: {e}")
|
||||||
|
|
||||||
def add_project(self, name: str) -> Project:
|
def add_project(self, name: str) -> Project:
|
||||||
"""Create new project."""
|
"""Create new project with default environment."""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
# Create default environment
|
||||||
|
default_env = Environment(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
name="Default",
|
||||||
|
variables={},
|
||||||
|
created_at=now
|
||||||
|
)
|
||||||
|
|
||||||
project = Project(
|
project = Project(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
name=name,
|
name=name,
|
||||||
requests=[],
|
requests=[],
|
||||||
created_at=datetime.now(timezone.utc).isoformat()
|
created_at=now,
|
||||||
|
variable_names=[],
|
||||||
|
environments=[default_env]
|
||||||
)
|
)
|
||||||
projects.append(project)
|
projects.append(project)
|
||||||
self.save_projects(projects)
|
self.save_projects(projects)
|
||||||
@ -154,3 +166,104 @@ class ProjectManager:
|
|||||||
p.requests = [r for r in p.requests if r.id != request_id]
|
p.requests = [r for r in p.requests if r.id != request_id]
|
||||||
break
|
break
|
||||||
self.save_projects(projects)
|
self.save_projects(projects)
|
||||||
|
|
||||||
|
def add_environment(self, project_id: str, name: str) -> Environment:
|
||||||
|
"""Add environment to a project."""
|
||||||
|
projects = self.load_projects()
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
environment = None
|
||||||
|
|
||||||
|
for p in projects:
|
||||||
|
if p.id == project_id:
|
||||||
|
# Create environment with empty values for all variables
|
||||||
|
variables = {var_name: "" for var_name in p.variable_names}
|
||||||
|
environment = Environment(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
name=name,
|
||||||
|
variables=variables,
|
||||||
|
created_at=now
|
||||||
|
)
|
||||||
|
p.environments.append(environment)
|
||||||
|
break
|
||||||
|
|
||||||
|
self.save_projects(projects)
|
||||||
|
return environment
|
||||||
|
|
||||||
|
def update_environment(self, project_id: str, env_id: str, name: str = None, variables: dict = None):
|
||||||
|
"""Update an environment's name and/or variables."""
|
||||||
|
projects = self.load_projects()
|
||||||
|
for p in projects:
|
||||||
|
if p.id == project_id:
|
||||||
|
for env in p.environments:
|
||||||
|
if env.id == env_id:
|
||||||
|
if name is not None:
|
||||||
|
env.name = name
|
||||||
|
if variables is not None:
|
||||||
|
env.variables = variables
|
||||||
|
break
|
||||||
|
break
|
||||||
|
self.save_projects(projects)
|
||||||
|
|
||||||
|
def delete_environment(self, project_id: str, env_id: str):
|
||||||
|
"""Delete an environment."""
|
||||||
|
projects = self.load_projects()
|
||||||
|
for p in projects:
|
||||||
|
if p.id == project_id:
|
||||||
|
p.environments = [e for e in p.environments if e.id != env_id]
|
||||||
|
break
|
||||||
|
self.save_projects(projects)
|
||||||
|
|
||||||
|
def add_variable(self, project_id: str, variable_name: str):
|
||||||
|
"""Add a variable to the project and all environments."""
|
||||||
|
projects = self.load_projects()
|
||||||
|
for p in projects:
|
||||||
|
if p.id == project_id:
|
||||||
|
if variable_name not in p.variable_names:
|
||||||
|
p.variable_names.append(variable_name)
|
||||||
|
# Add empty value to all environments
|
||||||
|
for env in p.environments:
|
||||||
|
env.variables[variable_name] = ""
|
||||||
|
break
|
||||||
|
self.save_projects(projects)
|
||||||
|
|
||||||
|
def rename_variable(self, project_id: str, old_name: str, new_name: str):
|
||||||
|
"""Rename a variable in the project and all environments."""
|
||||||
|
projects = self.load_projects()
|
||||||
|
for p in projects:
|
||||||
|
if p.id == project_id:
|
||||||
|
# Update variable_names list
|
||||||
|
if old_name in p.variable_names:
|
||||||
|
idx = p.variable_names.index(old_name)
|
||||||
|
p.variable_names[idx] = new_name
|
||||||
|
# Update all environments
|
||||||
|
for env in p.environments:
|
||||||
|
if old_name in env.variables:
|
||||||
|
env.variables[new_name] = env.variables.pop(old_name)
|
||||||
|
break
|
||||||
|
self.save_projects(projects)
|
||||||
|
|
||||||
|
def delete_variable(self, project_id: str, variable_name: str):
|
||||||
|
"""Delete a variable from the project and all environments."""
|
||||||
|
projects = self.load_projects()
|
||||||
|
for p in projects:
|
||||||
|
if p.id == project_id:
|
||||||
|
# Remove from variable_names
|
||||||
|
if variable_name in p.variable_names:
|
||||||
|
p.variable_names.remove(variable_name)
|
||||||
|
# Remove from all environments
|
||||||
|
for env in p.environments:
|
||||||
|
env.variables.pop(variable_name, None)
|
||||||
|
break
|
||||||
|
self.save_projects(projects)
|
||||||
|
|
||||||
|
def update_environment_variable(self, project_id: str, env_id: str, variable_name: str, value: str):
|
||||||
|
"""Update a single variable value in an environment."""
|
||||||
|
projects = self.load_projects()
|
||||||
|
for p in projects:
|
||||||
|
if p.id == project_id:
|
||||||
|
for env in p.environments:
|
||||||
|
if env.id == env_id:
|
||||||
|
env.variables[variable_name] = value
|
||||||
|
break
|
||||||
|
break
|
||||||
|
self.save_projects(projects)
|
||||||
|
|||||||
@ -5,9 +5,12 @@
|
|||||||
<file preprocess="xml-stripblanks">shortcuts-dialog.ui</file>
|
<file preprocess="xml-stripblanks">shortcuts-dialog.ui</file>
|
||||||
<file preprocess="xml-stripblanks">preferences-dialog.ui</file>
|
<file preprocess="xml-stripblanks">preferences-dialog.ui</file>
|
||||||
<file preprocess="xml-stripblanks">icon-picker-dialog.ui</file>
|
<file preprocess="xml-stripblanks">icon-picker-dialog.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">environments-dialog.ui</file>
|
||||||
<file preprocess="xml-stripblanks">widgets/header-row.ui</file>
|
<file preprocess="xml-stripblanks">widgets/header-row.ui</file>
|
||||||
<file preprocess="xml-stripblanks">widgets/history-item.ui</file>
|
<file preprocess="xml-stripblanks">widgets/history-item.ui</file>
|
||||||
<file preprocess="xml-stripblanks">widgets/project-item.ui</file>
|
<file preprocess="xml-stripblanks">widgets/project-item.ui</file>
|
||||||
<file preprocess="xml-stripblanks">widgets/request-item.ui</file>
|
<file preprocess="xml-stripblanks">widgets/request-item.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">widgets/variable-row.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">widgets/environment-row.ui</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
|
|||||||
@ -21,5 +21,7 @@ from .header_row import HeaderRow
|
|||||||
from .history_item import HistoryItem
|
from .history_item import HistoryItem
|
||||||
from .project_item import ProjectItem
|
from .project_item import ProjectItem
|
||||||
from .request_item import RequestItem
|
from .request_item import RequestItem
|
||||||
|
from .variable_row import VariableRow
|
||||||
|
from .environment_row import EnvironmentRow
|
||||||
|
|
||||||
__all__ = ['HeaderRow', 'HistoryItem', 'ProjectItem', 'RequestItem']
|
__all__ = ['HeaderRow', 'HistoryItem', 'ProjectItem', 'RequestItem', 'VariableRow', 'EnvironmentRow']
|
||||||
|
|||||||
52
src/widgets/environment-row.ui
Normal file
52
src/widgets/environment-row.ui
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk" version="4.0"/>
|
||||||
|
<template class="EnvironmentRow" parent="GtkBox">
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">12</property>
|
||||||
|
<property name="margin-start">12</property>
|
||||||
|
<property name="margin-end">12</property>
|
||||||
|
<property name="margin-top">6</property>
|
||||||
|
<property name="margin-bottom">6</property>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="name_label">
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="width-chars">15</property>
|
||||||
|
<style>
|
||||||
|
<class name="heading"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="values_box">
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="edit_button">
|
||||||
|
<property name="icon-name">document-edit-symbolic</property>
|
||||||
|
<property name="tooltip-text">Edit environment</property>
|
||||||
|
<signal name="clicked" handler="on_edit_clicked"/>
|
||||||
|
<style>
|
||||||
|
<class name="flat"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="delete_button">
|
||||||
|
<property name="icon-name">edit-delete-symbolic</property>
|
||||||
|
<property name="tooltip-text">Delete environment</property>
|
||||||
|
<signal name="clicked" handler="on_delete_clicked"/>
|
||||||
|
<style>
|
||||||
|
<class name="flat"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</template>
|
||||||
|
</interface>
|
||||||
86
src/widgets/environment_row.py
Normal file
86
src/widgets/environment_row.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# environment_row.py
|
||||||
|
#
|
||||||
|
# Copyright 2025 Pavel Baksy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from gi.repository import Gtk, GObject
|
||||||
|
|
||||||
|
|
||||||
|
@Gtk.Template(resource_path='/cz/vesp/roster/widgets/environment-row.ui')
|
||||||
|
class EnvironmentRow(Gtk.Box):
|
||||||
|
"""Widget for displaying and editing an environment with its variables."""
|
||||||
|
|
||||||
|
__gtype_name__ = 'EnvironmentRow'
|
||||||
|
|
||||||
|
name_label = Gtk.Template.Child()
|
||||||
|
values_box = Gtk.Template.Child()
|
||||||
|
edit_button = Gtk.Template.Child()
|
||||||
|
delete_button = Gtk.Template.Child()
|
||||||
|
|
||||||
|
__gsignals__ = {
|
||||||
|
'edit-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||||
|
'delete-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||||
|
'value-changed': (GObject.SIGNAL_RUN_FIRST, None, (str, str)) # variable_name, value
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, environment, variable_names):
|
||||||
|
super().__init__()
|
||||||
|
self.environment = environment
|
||||||
|
self.variable_names = variable_names
|
||||||
|
self.value_entries = {}
|
||||||
|
self._populate()
|
||||||
|
|
||||||
|
def _populate(self):
|
||||||
|
"""Populate with environment data."""
|
||||||
|
self.name_label.set_text(self.environment.name)
|
||||||
|
|
||||||
|
# Clear values box
|
||||||
|
while child := self.values_box.get_first_child():
|
||||||
|
self.values_box.remove(child)
|
||||||
|
|
||||||
|
# Create entry for each variable
|
||||||
|
for var_name in self.variable_names:
|
||||||
|
entry = Gtk.Entry()
|
||||||
|
entry.set_placeholder_text(var_name)
|
||||||
|
entry.set_text(self.environment.variables.get(var_name, ""))
|
||||||
|
entry.set_hexpand(True)
|
||||||
|
entry.connect('changed', self._on_value_changed, var_name)
|
||||||
|
self.values_box.append(entry)
|
||||||
|
self.value_entries[var_name] = entry
|
||||||
|
|
||||||
|
def _on_value_changed(self, entry, var_name):
|
||||||
|
"""Handle value entry changes."""
|
||||||
|
self.emit('value-changed', var_name, entry.get_text())
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def on_edit_clicked(self, button):
|
||||||
|
"""Handle edit button click."""
|
||||||
|
self.emit('edit-requested')
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def on_delete_clicked(self, button):
|
||||||
|
"""Handle delete button click."""
|
||||||
|
self.emit('delete-requested')
|
||||||
|
|
||||||
|
def refresh(self, variable_names):
|
||||||
|
"""Refresh widget with updated variable names."""
|
||||||
|
self.variable_names = variable_names
|
||||||
|
self._populate()
|
||||||
|
|
||||||
|
def get_values(self):
|
||||||
|
"""Get all variable values."""
|
||||||
|
return {var_name: entry.get_text() for var_name, entry in self.value_entries.items()}
|
||||||
@ -5,6 +5,10 @@
|
|||||||
|
|
||||||
<menu id="project_menu">
|
<menu id="project_menu">
|
||||||
<section>
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="label">Manage Environments</attribute>
|
||||||
|
<attribute name="action">project.manage-environments</attribute>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<attribute name="label">Add Request</attribute>
|
<attribute name="label">Add Request</attribute>
|
||||||
<attribute name="action">project.add-request</attribute>
|
<attribute name="action">project.add-request</attribute>
|
||||||
|
|||||||
@ -38,6 +38,7 @@ class ProjectItem(Gtk.Box):
|
|||||||
'add-request-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
'add-request-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||||
'request-load-requested': (GObject.SIGNAL_RUN_FIRST, None, (object,)),
|
'request-load-requested': (GObject.SIGNAL_RUN_FIRST, None, (object,)),
|
||||||
'request-delete-requested': (GObject.SIGNAL_RUN_FIRST, None, (object,)),
|
'request-delete-requested': (GObject.SIGNAL_RUN_FIRST, None, (object,)),
|
||||||
|
'manage-environments-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, project):
|
def __init__(self, project):
|
||||||
@ -56,6 +57,10 @@ class ProjectItem(Gtk.Box):
|
|||||||
"""Setup action group for menu."""
|
"""Setup action group for menu."""
|
||||||
actions = Gio.SimpleActionGroup.new()
|
actions = Gio.SimpleActionGroup.new()
|
||||||
|
|
||||||
|
action = Gio.SimpleAction.new("manage-environments", None)
|
||||||
|
action.connect("activate", lambda *_: self.emit('manage-environments-requested'))
|
||||||
|
actions.add_action(action)
|
||||||
|
|
||||||
action = Gio.SimpleAction.new("add-request", None)
|
action = Gio.SimpleAction.new("add-request", None)
|
||||||
action.connect("activate", lambda *_: self.emit('add-request-requested'))
|
action.connect("activate", lambda *_: self.emit('add-request-requested'))
|
||||||
actions.add_action(action)
|
actions.add_action(action)
|
||||||
|
|||||||
30
src/widgets/variable-row.ui
Normal file
30
src/widgets/variable-row.ui
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<requires lib="gtk" version="4.0"/>
|
||||||
|
<template class="VariableRow" parent="GtkBox">
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<property name="margin-start">6</property>
|
||||||
|
<property name="margin-end">6</property>
|
||||||
|
<property name="margin-top">3</property>
|
||||||
|
<property name="margin-bottom">3</property>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkEntry" id="name_entry">
|
||||||
|
<property name="placeholder-text">Variable name</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="remove_button">
|
||||||
|
<property name="icon-name">edit-delete-symbolic</property>
|
||||||
|
<property name="tooltip-text">Remove variable</property>
|
||||||
|
<signal name="clicked" handler="on_remove_clicked" swapped="no"/>
|
||||||
|
<style>
|
||||||
|
<class name="flat"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</template>
|
||||||
|
</interface>
|
||||||
57
src/widgets/variable_row.py
Normal file
57
src/widgets/variable_row.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# variable_row.py
|
||||||
|
#
|
||||||
|
# Copyright 2025 Pavel Baksy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from gi.repository import Gtk, GObject
|
||||||
|
|
||||||
|
|
||||||
|
@Gtk.Template(resource_path='/cz/vesp/roster/widgets/variable-row.ui')
|
||||||
|
class VariableRow(Gtk.Box):
|
||||||
|
"""Widget for editing a variable name."""
|
||||||
|
|
||||||
|
__gtype_name__ = 'VariableRow'
|
||||||
|
|
||||||
|
name_entry = Gtk.Template.Child()
|
||||||
|
remove_button = Gtk.Template.Child()
|
||||||
|
|
||||||
|
__gsignals__ = {
|
||||||
|
'remove-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||||
|
'changed': (GObject.SIGNAL_RUN_FIRST, None, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, variable_name=""):
|
||||||
|
super().__init__()
|
||||||
|
self.name_entry.set_text(variable_name)
|
||||||
|
self.name_entry.connect('changed', self._on_entry_changed)
|
||||||
|
|
||||||
|
def _on_entry_changed(self, entry):
|
||||||
|
"""Handle changes to name entry."""
|
||||||
|
self.emit('changed')
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def on_remove_clicked(self, button):
|
||||||
|
"""Handle remove button click."""
|
||||||
|
self.emit('remove-requested')
|
||||||
|
|
||||||
|
def get_variable_name(self):
|
||||||
|
"""Return variable name."""
|
||||||
|
return self.name_entry.get_text().strip()
|
||||||
|
|
||||||
|
def set_variable_name(self, name: str):
|
||||||
|
"""Set variable name."""
|
||||||
|
self.name_entry.set_text(name)
|
||||||
@ -26,6 +26,7 @@ from .history_manager import HistoryManager
|
|||||||
from .project_manager import ProjectManager
|
from .project_manager import ProjectManager
|
||||||
from .tab_manager import TabManager
|
from .tab_manager import TabManager
|
||||||
from .icon_picker_dialog import IconPickerDialog
|
from .icon_picker_dialog import IconPickerDialog
|
||||||
|
from .environments_dialog import EnvironmentsDialog
|
||||||
from .request_tab_widget import RequestTabWidget
|
from .request_tab_widget import RequestTabWidget
|
||||||
from .widgets.history_item import HistoryItem
|
from .widgets.history_item import HistoryItem
|
||||||
from .widgets.project_item import ProjectItem
|
from .widgets.project_item import ProjectItem
|
||||||
@ -528,6 +529,7 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
item.connect('add-request-requested', self._on_add_to_project, project)
|
item.connect('add-request-requested', self._on_add_to_project, project)
|
||||||
item.connect('request-load-requested', self._on_load_request)
|
item.connect('request-load-requested', self._on_load_request)
|
||||||
item.connect('request-delete-requested', self._on_delete_request, project)
|
item.connect('request-delete-requested', self._on_delete_request, project)
|
||||||
|
item.connect('manage-environments-requested', self._on_manage_environments, project)
|
||||||
self.projects_listbox.append(item)
|
self.projects_listbox.append(item)
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
@ -628,6 +630,17 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
dialog.connect("response", on_response)
|
dialog.connect("response", on_response)
|
||||||
dialog.present(self)
|
dialog.present(self)
|
||||||
|
|
||||||
|
def _on_manage_environments(self, widget, project):
|
||||||
|
"""Show environments management dialog."""
|
||||||
|
dialog = EnvironmentsDialog(project, self.project_manager)
|
||||||
|
|
||||||
|
def on_environments_updated(dlg):
|
||||||
|
# Reload projects to reflect changes
|
||||||
|
self._load_projects()
|
||||||
|
|
||||||
|
dialog.connect('environments-updated', on_environments_updated)
|
||||||
|
dialog.present(self)
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def on_save_request_clicked(self, button):
|
def on_save_request_clicked(self, button):
|
||||||
"""Save current request to a project."""
|
"""Save current request to a project."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user