mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-08-12 21:20:31 +02:00
fix(CLI): dock_area can be created from CLI with specific profile or empty
This commit is contained in:
@@ -39,7 +39,7 @@ def auto_update_dock_area(object_name: str | None = None) -> AutoUpdates:
|
|||||||
object_name(str): The name of the dock area.
|
object_name(str): The name of the dock area.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
BECDockArea: The created dock area.
|
AdvancedDockArea: The created dock area.
|
||||||
"""
|
"""
|
||||||
_auto_update = AutoUpdates(object_name=object_name)
|
_auto_update = AutoUpdates(object_name=object_name)
|
||||||
return _auto_update
|
return _auto_update
|
||||||
|
|||||||
@@ -397,12 +397,10 @@ class LaunchWindow(BECMainWindow):
|
|||||||
with RPCRegister.delayed_broadcast() as rpc_register:
|
with RPCRegister.delayed_broadcast() as rpc_register:
|
||||||
existing_dock_areas = rpc_register.get_names_of_rpc_by_class_type(AdvancedDockArea)
|
existing_dock_areas = rpc_register.get_names_of_rpc_by_class_type(AdvancedDockArea)
|
||||||
if name is not None:
|
if name is not None:
|
||||||
if name in existing_dock_areas:
|
|
||||||
raise ValueError(
|
|
||||||
f"Name {name} must be unique for dock areas, but already exists: {existing_dock_areas}."
|
|
||||||
)
|
|
||||||
WidgetContainerUtils.raise_for_invalid_name(name)
|
WidgetContainerUtils.raise_for_invalid_name(name)
|
||||||
|
# If name already exists, generate a unique one with counter suffix
|
||||||
|
if name in existing_dock_areas:
|
||||||
|
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
|
||||||
else:
|
else:
|
||||||
name = "dock_area"
|
name = "dock_area"
|
||||||
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
|
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
|
||||||
|
|||||||
@@ -274,6 +274,24 @@ class AdvancedDockArea(RPCBase):
|
|||||||
name (str | None): The name of the profile to load. If None, prompts the user.
|
name (str | None): The name of the profile to load. If None, prompts the user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def delete_profile(self, name: "str | None" = None, show_dialog: "bool" = False) -> "bool":
|
||||||
|
"""
|
||||||
|
Delete a workspace profile.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the profile to delete. If None, uses the currently
|
||||||
|
selected profile from the toolbar combo box (for UI usage).
|
||||||
|
show_dialog: If True, show confirmation dialog before deletion.
|
||||||
|
Defaults to False for CLI/programmatic usage.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the profile was deleted, False otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the profile is read-only or doesn't exist (when show_dialog=False).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class AutoUpdates(RPCBase):
|
class AutoUpdates(RPCBase):
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -303,27 +303,55 @@ class BECGuiClient(RPCBase):
|
|||||||
wait: bool = True,
|
wait: bool = True,
|
||||||
geometry: tuple[int, int, int, int] | None = None,
|
geometry: tuple[int, int, int, int] | None = None,
|
||||||
launch_script: str = "dock_area",
|
launch_script: str = "dock_area",
|
||||||
|
profile: str | None = None,
|
||||||
|
empty: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> client.BECDockArea:
|
) -> client.AdvancedDockArea:
|
||||||
"""Create a new top-level dock area.
|
"""Create a new top-level dock area.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name(str, optional): The name of the dock area. Defaults to None.
|
name(str, optional): The name of the dock area. Defaults to None.
|
||||||
wait(bool, optional): Whether to wait for the server to start. Defaults to True.
|
wait(bool, optional): Whether to wait for the server to start. Defaults to True.
|
||||||
geometry(tuple[int, int, int, int] | None): The geometry of the dock area (pos_x, pos_y, w, h)
|
geometry(tuple[int, int, int, int] | None): The geometry of the dock area (pos_x, pos_y, w, h).
|
||||||
|
launch_script(str): The launch script to use. Defaults to "dock_area".
|
||||||
|
profile(str | None): The profile name to load. If None, restores the last used profile.
|
||||||
|
Use a profile name to load a specific saved profile.
|
||||||
|
empty(bool): If True, start with an empty dock area without loading any profile.
|
||||||
|
This takes precedence over the profile argument. Defaults to False.
|
||||||
|
**kwargs: Additional keyword arguments passed to the dock area.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
client.BECDockArea: The new dock area.
|
client.AdvancedDockArea: The new dock area.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> gui.new() # Restore last used profile
|
||||||
|
>>> gui.new(profile="my_profile") # Load specific profile, if profile do not exist, the new profile is created empty with specified name
|
||||||
|
>>> gui.new(empty=True) # Start with empty dock area
|
||||||
|
>>> gui.new(name="custom_dock", empty=True) # Named empty dock area
|
||||||
"""
|
"""
|
||||||
|
if empty:
|
||||||
|
# Use a unique non-existent profile name to ensure empty start
|
||||||
|
profile = "__empty__"
|
||||||
if not self._check_if_server_is_alive():
|
if not self._check_if_server_is_alive():
|
||||||
self.start(wait=True)
|
self.start(wait=True)
|
||||||
if wait:
|
if wait:
|
||||||
with wait_for_server(self):
|
with wait_for_server(self):
|
||||||
widget = self.launcher._run_rpc(
|
widget = self.launcher._run_rpc(
|
||||||
"launch", launch_script=launch_script, name=name, geometry=geometry, **kwargs
|
"launch",
|
||||||
|
launch_script=launch_script,
|
||||||
|
name=name,
|
||||||
|
geometry=geometry,
|
||||||
|
profile=profile,
|
||||||
|
**kwargs,
|
||||||
) # pylint: disable=protected-access
|
) # pylint: disable=protected-access
|
||||||
return widget
|
return widget
|
||||||
widget = self.launcher._run_rpc(
|
widget = self.launcher._run_rpc(
|
||||||
"launch", launch_script=launch_script, name=name, geometry=geometry, **kwargs
|
"launch",
|
||||||
|
launch_script=launch_script,
|
||||||
|
name=name,
|
||||||
|
geometry=geometry,
|
||||||
|
profile=profile,
|
||||||
|
**kwargs,
|
||||||
) # pylint: disable=protected-access
|
) # pylint: disable=protected-access
|
||||||
return widget
|
return widget
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ class AdvancedDockArea(DockAreaWidget):
|
|||||||
"mode.setter",
|
"mode.setter",
|
||||||
"save_profile",
|
"save_profile",
|
||||||
"load_profile",
|
"load_profile",
|
||||||
|
"delete_profile",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Define a signal for mode changes
|
# Define a signal for mode changes
|
||||||
@@ -183,6 +184,7 @@ class AdvancedDockArea(DockAreaWidget):
|
|||||||
def _ensure_initial_profile(self) -> bool:
|
def _ensure_initial_profile(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Ensure at least one workspace profile exists for the current namespace.
|
Ensure at least one workspace profile exists for the current namespace.
|
||||||
|
If list_profile fails due to file permission or corrupted profiles, no action taken.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if a profile was created, False otherwise.
|
bool: True if a profile was created, False otherwise.
|
||||||
@@ -870,37 +872,82 @@ class AdvancedDockArea(DockAreaWidget):
|
|||||||
self.load_profile(target)
|
self.load_profile(target)
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def delete_profile(self):
|
def delete_profile(self, name: str | None = None, show_dialog: bool = False) -> bool:
|
||||||
"""
|
"""
|
||||||
Delete the currently selected workspace profile file and refresh the combo list.
|
Delete a workspace profile.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the profile to delete. If None, uses the currently
|
||||||
|
selected profile from the toolbar combo box (for UI usage).
|
||||||
|
show_dialog: If True, show confirmation dialog before deletion.
|
||||||
|
Defaults to False for CLI/programmatic usage.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the profile was deleted, False otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the profile is read-only or doesn't exist (when show_dialog=False).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
combo = self.toolbar.components.get_action("workspace_combo").widget
|
# Resolve profile name
|
||||||
name = combo.currentText()
|
if name is None:
|
||||||
|
combo = self.toolbar.components.get_action("workspace_combo").widget
|
||||||
|
name = combo.currentText()
|
||||||
if not name:
|
if not name:
|
||||||
return
|
if show_dialog:
|
||||||
|
return False
|
||||||
# Protect bundled/module/plugin profiles from deletion
|
raise ValueError("No profile name provided.")
|
||||||
if is_profile_read_only(name, namespace=self.profile_namespace):
|
|
||||||
QMessageBox.information(
|
|
||||||
self, "Delete Profile", f"Profile '{name}' is read-only and cannot be deleted."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Confirm deletion for regular profiles
|
|
||||||
reply = QMessageBox.question(
|
|
||||||
self,
|
|
||||||
"Delete Profile",
|
|
||||||
f"Are you sure you want to delete the profile '{name}'?\n\n"
|
|
||||||
f"This action cannot be undone.",
|
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
||||||
QMessageBox.StandardButton.No,
|
|
||||||
)
|
|
||||||
if reply != QMessageBox.StandardButton.Yes:
|
|
||||||
return
|
|
||||||
|
|
||||||
namespace = self.profile_namespace
|
namespace = self.profile_namespace
|
||||||
delete_profile_files(name, namespace=namespace)
|
|
||||||
|
# Check if profile is read-only
|
||||||
|
if is_profile_read_only(name, namespace=namespace):
|
||||||
|
if show_dialog:
|
||||||
|
QMessageBox.information(
|
||||||
|
self, "Delete Profile", f"Profile '{name}' is read-only and cannot be deleted."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
raise ValueError(f"Profile '{name}' is read-only and cannot be deleted.")
|
||||||
|
|
||||||
|
# Confirm deletion if dialog is enabled
|
||||||
|
if show_dialog:
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Delete Profile",
|
||||||
|
f"Are you sure you want to delete the profile '{name}'?\n\n"
|
||||||
|
f"This action cannot be undone.",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
|
QMessageBox.StandardButton.No,
|
||||||
|
)
|
||||||
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Perform deletion
|
||||||
|
try:
|
||||||
|
removed = delete_profile_files(name, namespace=namespace)
|
||||||
|
except OSError as exc:
|
||||||
|
if show_dialog:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self, "Delete Profile", f"Failed to delete profile '{name}': {exc}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
raise ValueError(f"Failed to delete profile '{name}': {exc}") from exc
|
||||||
|
|
||||||
|
if not removed:
|
||||||
|
if show_dialog:
|
||||||
|
QMessageBox.information(
|
||||||
|
self, "Delete Profile", "No writable profile files were found to delete."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
raise ValueError(f"No writable profile files found for '{name}'.")
|
||||||
|
|
||||||
|
# Clear current profile if it was deleted
|
||||||
|
if getattr(self, "_current_profile_name", None) == name:
|
||||||
|
self._current_profile_name = None
|
||||||
|
|
||||||
|
# Refresh the workspace list
|
||||||
self._refresh_workspace_list()
|
self._refresh_workspace_list()
|
||||||
|
return True
|
||||||
|
|
||||||
def _refresh_workspace_list(self):
|
def _refresh_workspace_list(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ from qtpy.QtWidgets import (
|
|||||||
from bec_widgets import BECWidget, SafeSlot
|
from bec_widgets import BECWidget, SafeSlot
|
||||||
from bec_widgets.utils.colors import get_accent_colors
|
from bec_widgets.utils.colors import get_accent_colors
|
||||||
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import (
|
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import (
|
||||||
delete_profile_files,
|
|
||||||
get_profile_info,
|
get_profile_info,
|
||||||
is_quick_select,
|
is_quick_select,
|
||||||
list_profiles,
|
list_profiles,
|
||||||
@@ -341,55 +340,35 @@ class WorkSpaceManager(BECWidget, QWidget):
|
|||||||
|
|
||||||
@SafeSlot(str)
|
@SafeSlot(str)
|
||||||
def delete_profile(self, profile_name: str):
|
def delete_profile(self, profile_name: str):
|
||||||
info = get_profile_info(profile_name, namespace=self.profile_namespace)
|
"""
|
||||||
if info.is_read_only:
|
Delete a profile by delegating to the target widget's delete_profile method.
|
||||||
QMessageBox.information(
|
|
||||||
self, "Delete Profile", "This profile is read-only and cannot be deleted."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
reply = QMessageBox.question(
|
Args:
|
||||||
self,
|
profile_name: The name of the profile to delete.
|
||||||
"Delete Profile",
|
"""
|
||||||
(
|
if self.target_widget is None or not hasattr(self.target_widget, "delete_profile"):
|
||||||
f"Delete the profile '{profile_name}'?\n\n"
|
QMessageBox.warning(
|
||||||
"This will remove both the user and default copies."
|
self, "Delete Profile", "No target widget available for profile deletion."
|
||||||
),
|
)
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
|
||||||
QMessageBox.No,
|
|
||||||
)
|
|
||||||
if reply != QMessageBox.Yes:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
removed = delete_profile_files(profile_name, namespace=self.profile_namespace)
|
result = self.target_widget.delete_profile(profile_name, show_dialog=True)
|
||||||
except OSError as exc:
|
except ValueError:
|
||||||
QMessageBox.warning(
|
# Error was already handled by target widget's dialog
|
||||||
self, "Delete Profile", f"Failed to delete profile '{profile_name}': {exc}"
|
result = False
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not removed:
|
if result:
|
||||||
QMessageBox.information(
|
# Refresh our table and select next profile
|
||||||
self, "Delete Profile", "No writable profile files were found to delete."
|
self.render_table()
|
||||||
)
|
remaining_profiles = list_profiles(namespace=self.profile_namespace)
|
||||||
return
|
if remaining_profiles:
|
||||||
|
next_profile = remaining_profiles[0]
|
||||||
if self.target_widget is not None:
|
self._select_by_name(next_profile)
|
||||||
if getattr(self.target_widget, "_current_profile_name", None) == profile_name:
|
self._show_profile_details(next_profile)
|
||||||
self.target_widget._current_profile_name = None
|
else:
|
||||||
if hasattr(self.target_widget, "_refresh_workspace_list"):
|
self.profile_details_tree.clear()
|
||||||
self.target_widget._refresh_workspace_list()
|
self.screenshot_label.setPixmap(QPixmap())
|
||||||
|
|
||||||
self.render_table()
|
|
||||||
remaining_profiles = list_profiles(namespace=self.profile_namespace)
|
|
||||||
if remaining_profiles:
|
|
||||||
next_profile = remaining_profiles[0]
|
|
||||||
self._select_by_name(next_profile)
|
|
||||||
self._show_profile_details(next_profile)
|
|
||||||
else:
|
|
||||||
self.profile_details_tree.clear()
|
|
||||||
self.screenshot_label.setPixmap(QPixmap())
|
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
def resizeEvent(self, event):
|
||||||
super().resizeEvent(event)
|
super().resizeEvent(event)
|
||||||
|
|||||||
@@ -141,9 +141,37 @@ def workspace_manager_target():
|
|||||||
def save_profile(self):
|
def save_profile(self):
|
||||||
self.save_called = True
|
self.save_called = True
|
||||||
|
|
||||||
|
def save_profile_dialog(self, name: str | None = None):
|
||||||
|
"""Mock save_profile_dialog that sets save_called flag."""
|
||||||
|
self.save_called = True
|
||||||
|
|
||||||
def _refresh_workspace_list(self):
|
def _refresh_workspace_list(self):
|
||||||
self.refresh_calls += 1
|
self.refresh_calls += 1
|
||||||
|
|
||||||
|
def delete_profile(self, name: str, show_dialog: bool = False) -> bool:
|
||||||
|
"""Mock delete_profile that performs actual file deletion."""
|
||||||
|
from qtpy.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import (
|
||||||
|
delete_profile_files,
|
||||||
|
is_profile_read_only,
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_profile_read_only(name):
|
||||||
|
if show_dialog:
|
||||||
|
QMessageBox.information(
|
||||||
|
None,
|
||||||
|
"Delete Profile",
|
||||||
|
f"Profile '{name}' is read-only and cannot be deleted.",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
raise ValueError(f"Profile '{name}' is read-only.")
|
||||||
|
delete_profile_files(name)
|
||||||
|
if self._current_profile_name == name:
|
||||||
|
self._current_profile_name = None
|
||||||
|
self._refresh_workspace_list()
|
||||||
|
return True
|
||||||
|
|
||||||
def _factory():
|
def _factory():
|
||||||
return _Target()
|
return _Target()
|
||||||
|
|
||||||
@@ -273,11 +301,6 @@ class TestBasicDockArea:
|
|||||||
assert "panel_one" not in basic_dock_area.dock_map()
|
assert "panel_one" not in basic_dock_area.dock_map()
|
||||||
assert "panel_two" in basic_dock_area.dock_map()
|
assert "panel_two" in basic_dock_area.dock_map()
|
||||||
|
|
||||||
def test_remove_widget_raises_for_unknown_name(self, basic_dock_area):
|
|
||||||
"""Test remove_widget raises ValueError for non-existent widget."""
|
|
||||||
with pytest.raises(ValueError, match="No widget found with object name 'nonexistent'"):
|
|
||||||
basic_dock_area.remove_widget("nonexistent")
|
|
||||||
|
|
||||||
def test_manifest_serialization_includes_floating_geometry(
|
def test_manifest_serialization_includes_floating_geometry(
|
||||||
self, basic_dock_area, qtbot, tmp_path
|
self, basic_dock_area, qtbot, tmp_path
|
||||||
):
|
):
|
||||||
@@ -1703,7 +1726,7 @@ class TestWorkspaceProfileOperations:
|
|||||||
"bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.SaveProfileDialog",
|
"bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.SaveProfileDialog",
|
||||||
StubDialog,
|
StubDialog,
|
||||||
):
|
):
|
||||||
advanced_dock_area.save_profile(profile_name)
|
advanced_dock_area.save_profile(profile_name, show_dialog=True)
|
||||||
|
|
||||||
assert os.path.exists(target_path)
|
assert os.path.exists(target_path)
|
||||||
|
|
||||||
@@ -1775,7 +1798,7 @@ class TestWorkspaceProfileOperations:
|
|||||||
"bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.SaveProfileDialog",
|
"bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.SaveProfileDialog",
|
||||||
StubDialog,
|
StubDialog,
|
||||||
):
|
):
|
||||||
advanced_dock_area.save_profile()
|
advanced_dock_area.save_profile(show_dialog=True)
|
||||||
|
|
||||||
qtbot.wait(500)
|
qtbot.wait(500)
|
||||||
source_manifest = read_manifest(helper.open_user(source_profile))
|
source_manifest = read_manifest(helper.open_user(source_profile))
|
||||||
@@ -1844,7 +1867,7 @@ class TestWorkspaceProfileOperations:
|
|||||||
return_value=None,
|
return_value=None,
|
||||||
) as mock_info,
|
) as mock_info,
|
||||||
):
|
):
|
||||||
advanced_dock_area.delete_profile()
|
advanced_dock_area.delete_profile(show_dialog=True)
|
||||||
|
|
||||||
mock_question.assert_not_called()
|
mock_question.assert_not_called()
|
||||||
mock_info.assert_called_once()
|
mock_info.assert_called_once()
|
||||||
@@ -1875,13 +1898,31 @@ class TestWorkspaceProfileOperations:
|
|||||||
mock_question.return_value = QMessageBox.Yes
|
mock_question.return_value = QMessageBox.Yes
|
||||||
|
|
||||||
with patch.object(advanced_dock_area, "_refresh_workspace_list") as mock_refresh:
|
with patch.object(advanced_dock_area, "_refresh_workspace_list") as mock_refresh:
|
||||||
advanced_dock_area.delete_profile()
|
advanced_dock_area.delete_profile(show_dialog=True)
|
||||||
|
|
||||||
mock_question.assert_called_once()
|
mock_question.assert_called_once()
|
||||||
mock_refresh.assert_called_once()
|
mock_refresh.assert_called_once()
|
||||||
# Profile should be deleted
|
# Profile should be deleted
|
||||||
assert not os.path.exists(user_path)
|
assert not os.path.exists(user_path)
|
||||||
|
|
||||||
|
def test_delete_profile_cli_usage(self, advanced_dock_area, temp_profile_dir):
|
||||||
|
"""Test delete_profile with explicit name (CLI usage - no dialog by default)."""
|
||||||
|
profile_name = "cli_deletable_profile"
|
||||||
|
helper = profile_helper(advanced_dock_area)
|
||||||
|
|
||||||
|
# Create regular profile
|
||||||
|
settings = helper.open_user(profile_name)
|
||||||
|
settings.setValue("test", "value")
|
||||||
|
settings.sync()
|
||||||
|
user_path = helper.user_path(profile_name)
|
||||||
|
assert os.path.exists(user_path)
|
||||||
|
|
||||||
|
# Delete without dialog (CLI usage - default behavior)
|
||||||
|
result = advanced_dock_area.delete_profile(profile_name)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert not os.path.exists(user_path)
|
||||||
|
|
||||||
def test_refresh_workspace_list(self, advanced_dock_area, temp_profile_dir):
|
def test_refresh_workspace_list(self, advanced_dock_area, temp_profile_dir):
|
||||||
"""Test refreshing workspace list."""
|
"""Test refreshing workspace list."""
|
||||||
# Create some profiles
|
# Create some profiles
|
||||||
|
|||||||
Reference in New Issue
Block a user