Skip to content

Commit 21e75a5

Browse files
committed
Can create, edit, and delete an alias in the UI
In particular: * Add NewAliasDialog Resolves #201
1 parent 655b580 commit 21e75a5

File tree

6 files changed

+982
-128
lines changed

6 files changed

+982
-128
lines changed

RELEASE_NOTES.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ Release Notes ⋮
1212

1313
### main
1414

15+
* Workflow improvements
16+
* Aliases are now supported for redirecting similar patterns of URLs,
17+
such as `https://www.folklore.org/ → https://folklore.org/`.
18+
* Use the "New Alias..." menuitem in the "Entity" menu to create an Alias.
19+
* Aliases can be used to redirect to external URLs outside the project,
20+
on the live internet.
21+
* This can be useful if a subset of a project's URLs are hosted in
22+
a different internet-accessible subsystem, such as AWS Glacier.
23+
1524
* Shell improvements
1625
* Uses the new [interactive interpreter] from Python 3.13.
1726
* Supports using `await` with Crystal's testing utilities

src/crystal/browser/__init__.py

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from crystal.app_preferences import app_prefs
77
from crystal.browser.entitytree import EntityTree
88
from crystal.browser.icons import TREE_NODE_ICONS
9+
from crystal.browser.new_alias import NewAliasDialog
910
from crystal.browser.new_group import NewGroupDialog
1011
from crystal.browser.new_root_url import ChangePrefixCommand, NewRootUrlDialog
1112
from crystal.browser.preferences import PreferencesDialog
@@ -316,6 +317,11 @@ def _create_actions(self) -> None:
316317
enabled=(not self._readonly),
317318
button_bitmap=dict(DEFAULT_FOLDER_ICON_SET())[wx.TreeItemIcon_Normal],
318319
button_label='New &Group...')
320+
self._new_alias_action = Action(wx.ID_ANY,
321+
'New &Alias...',
322+
wx.AcceleratorEntry(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord('A')),
323+
self._on_new_alias,
324+
enabled=(not self._readonly))
319325
self._edit_action = Action(wx.ID_ANY,
320326
self._EDIT_ACTION_LABEL if not self._readonly else self._GET_INFO_ACTION_LABEL,
321327
accel=wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_RETURN),
@@ -438,6 +444,7 @@ def _create_menu_bar(self, raw_frame: wx.Frame) -> wx.MenuBar:
438444
entity_menu.AppendSeparator()
439445
self._new_root_url_action.append_menuitem_to(entity_menu)
440446
self._new_group_action.append_menuitem_to(entity_menu)
447+
self._new_alias_action.append_menuitem_to(entity_menu)
441448
self._edit_action.append_menuitem_to(entity_menu)
442449
self._forget_action.append_menuitem_to(entity_menu)
443450
entity_menu.AppendSeparator()
@@ -1413,16 +1420,56 @@ def _on_edit_group_dialog_ok(self,
14131420

14141421
assert download_immediately == False
14151422

1416-
def _saving_source_would_create_cycle(self, rg: ResourceGroup, source: ResourceGroupSource) -> bool:
1417-
ancestor_source = source # type: ResourceGroupSource
1418-
while ancestor_source is not None:
1419-
if ancestor_source == rg:
1420-
return True
1421-
if isinstance(ancestor_source, ResourceGroup):
1422-
ancestor_source = ancestor_source.source # reinterpret
1423-
else:
1424-
ancestor_source = None
1425-
return False
1423+
# === Entity Pane: New/Edit Alias ===
1424+
1425+
def _on_new_alias(self, event: wx.CommandEvent) -> None:
1426+
restore_menuitems = self._disable_menus_during_showwindowmodal()
1427+
NewAliasDialog(
1428+
self._frame,
1429+
self._on_new_alias_dialog_ok,
1430+
alias_exists_func=self._alias_exists,
1431+
on_close=restore_menuitems,
1432+
)
1433+
1434+
def _alias_exists(self, source_url_prefix: str) -> bool:
1435+
return self.project.get_alias(source_url_prefix) is not None
1436+
1437+
@fg_affinity
1438+
def _on_new_alias_dialog_ok(self,
1439+
source_url_prefix: str,
1440+
target_url_prefix: str,
1441+
target_is_external: bool,
1442+
) -> None:
1443+
if source_url_prefix == '':
1444+
raise ValueError('Invalid blank source URL prefix')
1445+
if target_url_prefix == '':
1446+
raise ValueError('Invalid blank target URL prefix')
1447+
1448+
try:
1449+
alias = Alias(self.project, source_url_prefix, target_url_prefix,
1450+
target_is_external=target_is_external)
1451+
except Alias.AlreadyExists:
1452+
raise ValueError('Invalid duplicate source URL prefix')
1453+
1454+
@fg_affinity
1455+
def _on_edit_alias_dialog_ok(self,
1456+
alias: Alias,
1457+
source_url_prefix: str,
1458+
target_url_prefix: str,
1459+
target_is_external: bool,
1460+
) -> None:
1461+
if source_url_prefix != alias.source_url_prefix:
1462+
raise ValueError(
1463+
'Attempted to change source_url_prefix of existing Alias. '
1464+
'Source URL prefix cannot be changed after alias is created.'
1465+
)
1466+
1467+
if target_url_prefix == '':
1468+
raise ValueError('Invalid blank target URL prefix')
1469+
1470+
# NOTE: Entity tree update will be triggered by property setters
1471+
alias.target_url_prefix = target_url_prefix
1472+
alias.target_is_external = target_is_external
14261473

14271474
# === Entity Pane: Other Commands ===
14281475

@@ -1487,9 +1534,38 @@ def _on_edit_entity(self, event) -> None:
14871534
raise
14881535
except CancelLoadUrls:
14891536
pass
1537+
elif isinstance(selected_entity, Alias):
1538+
alias = selected_entity
1539+
restore_menuitems = self._disable_menus_during_showwindowmodal()
1540+
try:
1541+
NewAliasDialog(
1542+
self._frame,
1543+
partial(self._on_edit_alias_dialog_ok, alias),
1544+
alias_exists_func=self._alias_exists,
1545+
initial_source_url_prefix=alias.source_url_prefix,
1546+
initial_target_url_prefix=alias.target_url_prefix,
1547+
initial_target_is_external=alias.target_is_external,
1548+
is_edit=True,
1549+
readonly=self._readonly,
1550+
on_close=restore_menuitems,
1551+
)
1552+
except:
1553+
restore_menuitems()
1554+
raise
14901555
else:
14911556
raise AssertionError()
14921557

1558+
def _saving_source_would_create_cycle(self, rg: ResourceGroup, source: ResourceGroupSource) -> bool:
1559+
ancestor_source = source # type: ResourceGroupSource
1560+
while ancestor_source is not None:
1561+
if ancestor_source == rg:
1562+
return True
1563+
if isinstance(ancestor_source, ResourceGroup):
1564+
ancestor_source = ancestor_source.source # reinterpret
1565+
else:
1566+
ancestor_source = None
1567+
return False
1568+
14931569
def _on_forget_entity(self, event) -> None:
14941570
selected_entity = self.entity_tree.selected_entity
14951571
assert selected_entity is not None

0 commit comments

Comments
 (0)