Implement environment variable substitution in requests
This commit is contained in:
parent
24b1db4b9b
commit
2ba3ced14e
@ -31,6 +31,7 @@ roster_sources = [
|
|||||||
'main.py',
|
'main.py',
|
||||||
'window.py',
|
'window.py',
|
||||||
'models.py',
|
'models.py',
|
||||||
|
'variable_substitution.py',
|
||||||
'http_client.py',
|
'http_client.py',
|
||||||
'history_manager.py',
|
'history_manager.py',
|
||||||
'project_manager.py',
|
'project_manager.py',
|
||||||
|
|||||||
@ -203,6 +203,8 @@ class RequestTab:
|
|||||||
saved_request_id: Optional[str] = None # ID if from SavedRequest
|
saved_request_id: Optional[str] = None # ID if from SavedRequest
|
||||||
modified: bool = False # True if has unsaved changes
|
modified: bool = False # True if has unsaved changes
|
||||||
original_request: Optional[HttpRequest] = None # For change detection
|
original_request: Optional[HttpRequest] = None # For change detection
|
||||||
|
project_id: Optional[str] = None # Project association for environment variables
|
||||||
|
selected_environment_id: Optional[str] = None # Selected environment for variable substitution
|
||||||
|
|
||||||
def is_modified(self) -> bool:
|
def is_modified(self) -> bool:
|
||||||
"""Check if current request differs from original."""
|
"""Check if current request differs from original."""
|
||||||
@ -227,7 +229,9 @@ class RequestTab:
|
|||||||
'response': self.response.to_dict() if self.response else None,
|
'response': self.response.to_dict() if self.response else None,
|
||||||
'saved_request_id': self.saved_request_id,
|
'saved_request_id': self.saved_request_id,
|
||||||
'modified': self.modified,
|
'modified': self.modified,
|
||||||
'original_request': self.original_request.to_dict() if self.original_request else None
|
'original_request': self.original_request.to_dict() if self.original_request else None,
|
||||||
|
'project_id': self.project_id,
|
||||||
|
'selected_environment_id': self.selected_environment_id
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -240,5 +244,7 @@ class RequestTab:
|
|||||||
response=HttpResponse.from_dict(data['response']) if data.get('response') else None,
|
response=HttpResponse.from_dict(data['response']) if data.get('response') else None,
|
||||||
saved_request_id=data.get('saved_request_id'),
|
saved_request_id=data.get('saved_request_id'),
|
||||||
modified=data.get('modified', False),
|
modified=data.get('modified', False),
|
||||||
original_request=HttpRequest.from_dict(data['original_request']) if data.get('original_request') else None
|
original_request=HttpRequest.from_dict(data['original_request']) if data.get('original_request') else None,
|
||||||
|
project_id=data.get('project_id'),
|
||||||
|
selected_environment_id=data.get('selected_environment_id')
|
||||||
)
|
)
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import xml.dom.minidom
|
|||||||
class RequestTabWidget(Gtk.Box):
|
class RequestTabWidget(Gtk.Box):
|
||||||
"""Widget representing a single request tab's UI."""
|
"""Widget representing a single request tab's UI."""
|
||||||
|
|
||||||
def __init__(self, tab_id, request=None, response=None, **kwargs):
|
def __init__(self, tab_id, request=None, response=None, project_id=None, selected_environment_id=None, **kwargs):
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs)
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs)
|
||||||
|
|
||||||
self.tab_id = tab_id
|
self.tab_id = tab_id
|
||||||
@ -37,6 +37,10 @@ class RequestTabWidget(Gtk.Box):
|
|||||||
self.response = response
|
self.response = response
|
||||||
self.modified = False
|
self.modified = False
|
||||||
self.original_request = None
|
self.original_request = None
|
||||||
|
self.project_id = project_id
|
||||||
|
self.selected_environment_id = selected_environment_id
|
||||||
|
self.project_manager = None # Will be injected from window
|
||||||
|
self.environment_dropdown = None # Will be created if project_id is set
|
||||||
|
|
||||||
# Build the UI
|
# Build the UI
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
@ -83,6 +87,11 @@ class RequestTabWidget(Gtk.Box):
|
|||||||
url_clamp.set_child(url_box)
|
url_clamp.set_child(url_box)
|
||||||
self.append(url_clamp)
|
self.append(url_clamp)
|
||||||
|
|
||||||
|
# Environment Selector (only if project_id is set)
|
||||||
|
env_selector = self._build_environment_selector()
|
||||||
|
if env_selector:
|
||||||
|
self.append(env_selector)
|
||||||
|
|
||||||
# Vertical Paned: Main Content | History Panel
|
# Vertical Paned: Main Content | History Panel
|
||||||
vpane = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
|
vpane = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
|
||||||
vpane.set_vexpand(True)
|
vpane.set_vexpand(True)
|
||||||
@ -564,3 +573,105 @@ class RequestTabWidget(Gtk.Box):
|
|||||||
print(f"Failed to format body: {e}")
|
print(f"Failed to format body: {e}")
|
||||||
|
|
||||||
return body
|
return body
|
||||||
|
|
||||||
|
def _build_environment_selector(self):
|
||||||
|
"""Build environment selector UI. Returns None if no project_id."""
|
||||||
|
if not self.project_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create horizontal box for environment selector
|
||||||
|
env_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||||
|
env_box.set_margin_start(12)
|
||||||
|
env_box.set_margin_end(12)
|
||||||
|
env_box.set_margin_top(6)
|
||||||
|
env_box.set_margin_bottom(6)
|
||||||
|
|
||||||
|
# Label
|
||||||
|
label = Gtk.Label(label="Environment:")
|
||||||
|
label.add_css_class("dim-label")
|
||||||
|
env_box.append(label)
|
||||||
|
|
||||||
|
# Dropdown (will be populated when project_manager is injected)
|
||||||
|
self.environment_dropdown = Gtk.DropDown()
|
||||||
|
self.environment_dropdown.set_enable_search(False)
|
||||||
|
env_box.append(self.environment_dropdown)
|
||||||
|
|
||||||
|
# Connect signal
|
||||||
|
self.environment_dropdown.connect("notify::selected", self._on_environment_changed)
|
||||||
|
|
||||||
|
# Populate if project_manager is already available
|
||||||
|
if self.project_manager:
|
||||||
|
self._populate_environment_dropdown()
|
||||||
|
|
||||||
|
return env_box
|
||||||
|
|
||||||
|
def _populate_environment_dropdown(self):
|
||||||
|
"""Populate environment dropdown with project's environments."""
|
||||||
|
if not self.environment_dropdown or not self.project_manager or not self.project_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get project
|
||||||
|
projects = self.project_manager.load_projects()
|
||||||
|
project = None
|
||||||
|
for p in projects:
|
||||||
|
if p.id == self.project_id:
|
||||||
|
project = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if not project:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Build string list with "None" + environment names
|
||||||
|
string_list = Gtk.StringList()
|
||||||
|
string_list.append("None")
|
||||||
|
|
||||||
|
# Track environment IDs (index 0 is None)
|
||||||
|
self.environment_ids = [None]
|
||||||
|
|
||||||
|
for env in project.environments:
|
||||||
|
string_list.append(env.name)
|
||||||
|
self.environment_ids.append(env.id)
|
||||||
|
|
||||||
|
self.environment_dropdown.set_model(string_list)
|
||||||
|
|
||||||
|
# Select current environment
|
||||||
|
if self.selected_environment_id:
|
||||||
|
try:
|
||||||
|
index = self.environment_ids.index(self.selected_environment_id)
|
||||||
|
self.environment_dropdown.set_selected(index)
|
||||||
|
except ValueError:
|
||||||
|
self.environment_dropdown.set_selected(0) # Default to "None"
|
||||||
|
else:
|
||||||
|
self.environment_dropdown.set_selected(0) # Default to "None"
|
||||||
|
|
||||||
|
def _on_environment_changed(self, dropdown, _param):
|
||||||
|
"""Handle environment selection change."""
|
||||||
|
if not hasattr(self, 'environment_ids'):
|
||||||
|
return
|
||||||
|
|
||||||
|
selected_index = dropdown.get_selected()
|
||||||
|
if selected_index < len(self.environment_ids):
|
||||||
|
self.selected_environment_id = self.environment_ids[selected_index]
|
||||||
|
|
||||||
|
def get_selected_environment(self):
|
||||||
|
"""Get the currently selected environment object."""
|
||||||
|
if not self.selected_environment_id or not self.project_manager or not self.project_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Load projects
|
||||||
|
projects = self.project_manager.load_projects()
|
||||||
|
project = None
|
||||||
|
for p in projects:
|
||||||
|
if p.id == self.project_id:
|
||||||
|
project = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if not project:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find environment
|
||||||
|
for env in project.environments:
|
||||||
|
if env.id == self.selected_environment_id:
|
||||||
|
return env
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
@ -34,7 +34,9 @@ class TabManager:
|
|||||||
|
|
||||||
def create_tab(self, name: str, request: HttpRequest,
|
def create_tab(self, name: str, request: HttpRequest,
|
||||||
saved_request_id: Optional[str] = None,
|
saved_request_id: Optional[str] = None,
|
||||||
response: Optional[HttpResponse] = None) -> RequestTab:
|
response: Optional[HttpResponse] = None,
|
||||||
|
project_id: Optional[str] = None,
|
||||||
|
selected_environment_id: Optional[str] = None) -> RequestTab:
|
||||||
"""Create a new tab and add it to the collection."""
|
"""Create a new tab and add it to the collection."""
|
||||||
tab_id = str(uuid.uuid4())
|
tab_id = str(uuid.uuid4())
|
||||||
|
|
||||||
@ -58,7 +60,9 @@ class TabManager:
|
|||||||
response=response,
|
response=response,
|
||||||
saved_request_id=saved_request_id,
|
saved_request_id=saved_request_id,
|
||||||
modified=False,
|
modified=False,
|
||||||
original_request=original_request
|
original_request=original_request,
|
||||||
|
project_id=project_id,
|
||||||
|
selected_environment_id=selected_environment_id
|
||||||
)
|
)
|
||||||
|
|
||||||
self.tabs.append(tab)
|
self.tabs.append(tab)
|
||||||
|
|||||||
125
src/variable_substitution.py
Normal file
125
src/variable_substitution.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# variable_substitution.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
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Dict, List, Tuple, Set
|
||||||
|
from .models import HttpRequest, Environment
|
||||||
|
|
||||||
|
|
||||||
|
class VariableSubstitution:
|
||||||
|
"""Handles variable substitution with {{variable_name}} syntax."""
|
||||||
|
|
||||||
|
# Pattern to match {{variable_name}} where variable_name is alphanumeric + underscore
|
||||||
|
VARIABLE_PATTERN = re.compile(r'\{\{(\w+)\}\}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def find_variables(text: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Extract all {{variable_name}} references from text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The text to search for variable references
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of variable names found (without the {{ }} delimiters)
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return []
|
||||||
|
return VariableSubstitution.VARIABLE_PATTERN.findall(text)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def substitute(text: str, variables: Dict[str, str]) -> Tuple[str, List[str]]:
|
||||||
|
"""
|
||||||
|
Replace {{variable_name}} with values from variables dict.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The text containing variable placeholders
|
||||||
|
variables: Dictionary mapping variable names to their values
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (substituted_text, list_of_undefined_variables)
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return text, []
|
||||||
|
|
||||||
|
undefined_vars = []
|
||||||
|
|
||||||
|
def replace_var(match):
|
||||||
|
var_name = match.group(1)
|
||||||
|
if var_name in variables:
|
||||||
|
value = variables[var_name]
|
||||||
|
# Replace with value or empty string if value is None/empty
|
||||||
|
return value if value else ""
|
||||||
|
else:
|
||||||
|
# Variable not defined - track it and replace with empty string
|
||||||
|
undefined_vars.append(var_name)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
substituted = VariableSubstitution.VARIABLE_PATTERN.sub(replace_var, text)
|
||||||
|
return substituted, undefined_vars
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def substitute_request(request: HttpRequest, environment: Environment) -> Tuple[HttpRequest, Set[str]]:
|
||||||
|
"""
|
||||||
|
Substitute variables in all request fields (URL, headers, body).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The HTTP request with variable placeholders
|
||||||
|
environment: The environment containing variable values
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (new_request_with_substitutions, set_of_undefined_variables)
|
||||||
|
"""
|
||||||
|
all_undefined = set()
|
||||||
|
|
||||||
|
# Substitute URL
|
||||||
|
new_url, url_undefined = VariableSubstitution.substitute(
|
||||||
|
request.url, environment.variables
|
||||||
|
)
|
||||||
|
all_undefined.update(url_undefined)
|
||||||
|
|
||||||
|
# Substitute headers (both keys and values)
|
||||||
|
new_headers = {}
|
||||||
|
for key, value in request.headers.items():
|
||||||
|
new_key, key_undefined = VariableSubstitution.substitute(
|
||||||
|
key, environment.variables
|
||||||
|
)
|
||||||
|
new_value, value_undefined = VariableSubstitution.substitute(
|
||||||
|
value, environment.variables
|
||||||
|
)
|
||||||
|
all_undefined.update(key_undefined)
|
||||||
|
all_undefined.update(value_undefined)
|
||||||
|
new_headers[new_key] = new_value
|
||||||
|
|
||||||
|
# Substitute body
|
||||||
|
new_body, body_undefined = VariableSubstitution.substitute(
|
||||||
|
request.body, environment.variables
|
||||||
|
)
|
||||||
|
all_undefined.update(body_undefined)
|
||||||
|
|
||||||
|
# Create new HttpRequest with substituted values
|
||||||
|
new_request = HttpRequest(
|
||||||
|
method=request.method,
|
||||||
|
url=new_url,
|
||||||
|
headers=new_headers,
|
||||||
|
body=new_body,
|
||||||
|
syntax=request.syntax
|
||||||
|
)
|
||||||
|
|
||||||
|
return new_request, all_undefined
|
||||||
@ -150,6 +150,11 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
color: @accent_fg_color;
|
color: @accent_fg_color;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Warning styling for undefined variables */
|
||||||
|
entry.warning {
|
||||||
|
background-color: mix(@warning_bg_color, @view_bg_color, 0.3);
|
||||||
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
Gtk.StyleContext.add_provider_for_display(
|
Gtk.StyleContext.add_provider_for_display(
|
||||||
@ -294,18 +299,25 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
return f"{base_name} (copy {counter})"
|
return f"{base_name} (copy {counter})"
|
||||||
|
|
||||||
def _create_new_tab(self, name="New Request", request=None, saved_request_id=None, response=None):
|
def _create_new_tab(self, name="New Request", request=None, saved_request_id=None, response=None,
|
||||||
|
project_id=None, selected_environment_id=None):
|
||||||
"""Create a new tab with RequestTabWidget."""
|
"""Create a new tab with RequestTabWidget."""
|
||||||
# Create tab in tab manager
|
# Create tab in tab manager
|
||||||
if not request:
|
if not request:
|
||||||
request = HttpRequest(method="GET", url="", headers={}, body="", syntax="RAW")
|
request = HttpRequest(method="GET", url="", headers={}, body="", syntax="RAW")
|
||||||
|
|
||||||
tab = self.tab_manager.create_tab(name, request, saved_request_id, response)
|
tab = self.tab_manager.create_tab(name, request, saved_request_id, response,
|
||||||
|
project_id, selected_environment_id)
|
||||||
self.current_tab_id = tab.id
|
self.current_tab_id = tab.id
|
||||||
|
|
||||||
# Create RequestTabWidget for this tab
|
# Create RequestTabWidget for this tab
|
||||||
widget = RequestTabWidget(tab.id, request, response)
|
widget = RequestTabWidget(tab.id, request, response, project_id, selected_environment_id)
|
||||||
widget.original_request = tab.original_request
|
widget.original_request = tab.original_request
|
||||||
|
widget.project_manager = self.project_manager # Inject project manager
|
||||||
|
|
||||||
|
# Populate environment dropdown if project_id is set
|
||||||
|
if project_id and hasattr(widget, '_populate_environment_dropdown'):
|
||||||
|
widget._populate_environment_dropdown()
|
||||||
|
|
||||||
# Connect to send button
|
# Connect to send button
|
||||||
widget.send_button.connect("clicked", lambda btn: self._on_send_clicked(widget))
|
widget.send_button.connect("clicked", lambda btn: self._on_send_clicked(widget))
|
||||||
@ -327,6 +339,21 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
return tab.id
|
return tab.id
|
||||||
|
|
||||||
|
def _get_project_for_saved_request(self, saved_request_id):
|
||||||
|
"""Find which project contains a saved request. Returns (project_id, default_env_id) or (None, None)."""
|
||||||
|
if not saved_request_id:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
projects = self.project_manager.load_projects()
|
||||||
|
for project in projects:
|
||||||
|
for request in project.requests:
|
||||||
|
if request.id == saved_request_id:
|
||||||
|
# Found the project - get default environment (first one)
|
||||||
|
default_env_id = project.environments[0].id if project.environments else None
|
||||||
|
return project.id, default_env_id
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
def _on_tab_modified_changed(self, page, modified):
|
def _on_tab_modified_changed(self, page, modified):
|
||||||
"""Update tab indicator when modified state changes."""
|
"""Update tab indicator when modified state changes."""
|
||||||
# Update the tab title with/without modified indicator
|
# Update the tab title with/without modified indicator
|
||||||
@ -385,6 +412,17 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
self._show_toast("Please enter a URL")
|
self._show_toast("Please enter a URL")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Apply variable substitution if environment is selected
|
||||||
|
substituted_request = request
|
||||||
|
if widget.selected_environment_id:
|
||||||
|
env = widget.get_selected_environment()
|
||||||
|
if env:
|
||||||
|
from .variable_substitution import VariableSubstitution
|
||||||
|
substituted_request, undefined = VariableSubstitution.substitute_request(request, env)
|
||||||
|
# Log undefined variables for debugging
|
||||||
|
if undefined:
|
||||||
|
print(f"Warning: Undefined variables in request: {', '.join(undefined)}")
|
||||||
|
|
||||||
# Disable send button during request
|
# Disable send button during request
|
||||||
widget.send_button.set_sensitive(False)
|
widget.send_button.set_sensitive(False)
|
||||||
widget.send_button.set_label("Sending...")
|
widget.send_button.set_label("Sending...")
|
||||||
@ -421,7 +459,7 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
# Refresh history panel to show new entry
|
# Refresh history panel to show new entry
|
||||||
self._load_history()
|
self._load_history()
|
||||||
|
|
||||||
self.http_client.execute_request_async(request, callback, None)
|
self.http_client.execute_request_async(substituted_request, callback, None)
|
||||||
|
|
||||||
# History and Project Management
|
# History and Project Management
|
||||||
def _load_history(self):
|
def _load_history(self):
|
||||||
@ -874,10 +912,21 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
current_tab = self.page_to_tab.get(page)
|
current_tab = self.page_to_tab.get(page)
|
||||||
|
|
||||||
if widget and current_tab:
|
if widget and current_tab:
|
||||||
|
# Get project context
|
||||||
|
project_id, default_env_id = self._get_project_for_saved_request(link_to_saved)
|
||||||
|
|
||||||
# Update the tab metadata
|
# Update the tab metadata
|
||||||
current_tab.name = tab_name
|
current_tab.name = tab_name
|
||||||
current_tab.request = req
|
current_tab.request = req
|
||||||
current_tab.saved_request_id = link_to_saved
|
current_tab.saved_request_id = link_to_saved
|
||||||
|
current_tab.project_id = project_id
|
||||||
|
current_tab.selected_environment_id = default_env_id
|
||||||
|
|
||||||
|
# Update widget with project context
|
||||||
|
widget.project_id = project_id
|
||||||
|
widget.selected_environment_id = default_env_id
|
||||||
|
if project_id and hasattr(widget, '_populate_environment_dropdown'):
|
||||||
|
widget._populate_environment_dropdown()
|
||||||
|
|
||||||
# Update the tab page title
|
# Update the tab page title
|
||||||
page.set_title(tab_name)
|
page.set_title(tab_name)
|
||||||
@ -905,10 +954,13 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
else:
|
else:
|
||||||
# Current tab has changes or is not a "New Request"
|
# Current tab has changes or is not a "New Request"
|
||||||
# Create a new tab
|
# Create a new tab
|
||||||
|
project_id, default_env_id = self._get_project_for_saved_request(link_to_saved)
|
||||||
tab_id = self._create_new_tab(
|
tab_id = self._create_new_tab(
|
||||||
name=tab_name,
|
name=tab_name,
|
||||||
request=req,
|
request=req,
|
||||||
saved_request_id=link_to_saved
|
saved_request_id=link_to_saved,
|
||||||
|
project_id=project_id,
|
||||||
|
selected_environment_id=default_env_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# If it's a copy, clear the original_request to mark as unsaved
|
# If it's a copy, clear the original_request to mark as unsaved
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user