feat(create): server integration for silo viewer widgets #274
Submodule mods/silo updated: dca6380199...edbaf65923
@@ -12,6 +12,63 @@ import json
|
||||
import FreeCAD
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server integration — optional, degrades gracefully offline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_silo_client = None # SiloClient instance or None
|
||||
_silo_server_mode = None # callable returning "normal"/"read-only"/"offline"
|
||||
|
||||
|
||||
def _init_server():
|
||||
"""Lazy-import silo_commands and cache client + mode accessor."""
|
||||
global _silo_client, _silo_server_mode
|
||||
if _silo_server_mode is not None:
|
||||
return
|
||||
try:
|
||||
import silo_commands
|
||||
|
||||
_silo_client = silo_commands._client
|
||||
_silo_server_mode = lambda: silo_commands._server_mode
|
||||
except ImportError:
|
||||
_silo_server_mode = lambda: "offline"
|
||||
|
||||
|
||||
def _is_online():
|
||||
"""Check if silo server is reachable."""
|
||||
_init_server()
|
||||
return _silo_server_mode() == "normal"
|
||||
|
||||
|
||||
def _get_part_number(doc=None):
|
||||
"""Get the Silo part number from the document's tracked object."""
|
||||
if doc is None:
|
||||
doc = FreeCAD.ActiveDocument
|
||||
if doc is None:
|
||||
return None
|
||||
for obj in doc.Objects:
|
||||
if hasattr(obj, "SiloPartNumber") and obj.SiloPartNumber:
|
||||
return obj.SiloPartNumber
|
||||
return None
|
||||
|
||||
|
||||
def _make_offline_banner():
|
||||
"""Create a hidden banner label for offline state."""
|
||||
banner = QtWidgets.QLabel("Offline \u2014 showing cached data")
|
||||
banner.setStyleSheet(
|
||||
"QLabel { background: #f38ba8; color: #11111b; "
|
||||
"padding: 4px 8px; border-radius: 4px; font-size: 11px; }"
|
||||
)
|
||||
banner.setVisible(False)
|
||||
return banner
|
||||
|
||||
|
||||
def _update_banner(banner):
|
||||
"""Show banner if offline, hide if online."""
|
||||
_init_server()
|
||||
banner.setVisible(_silo_server_mode() != "normal")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Manifest Viewer
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -133,7 +190,7 @@ _LIFECYCLE_OPTIONS = ["draft", "review", "released", "obsolete"]
|
||||
|
||||
|
||||
class SiloMetadataEditor(QtWidgets.QWidget):
|
||||
"""Editable form for ``silo/metadata.json`` fields."""
|
||||
"""Editable form for ``silo/metadata.json`` with server write-back."""
|
||||
|
||||
WINDOW_TITLE = "Silo \u2014 Metadata"
|
||||
|
||||
@@ -147,8 +204,11 @@ class SiloMetadataEditor(QtWidgets.QWidget):
|
||||
self._lifecycle_combo = None
|
||||
self._save_btn = None
|
||||
self._reset_btn = None
|
||||
self._banner = None
|
||||
self._event_listener = None
|
||||
self._schema_name = ""
|
||||
self._build_ui(obj.RawContent)
|
||||
self._start_sse()
|
||||
|
||||
# -- layout --------------------------------------------------------------
|
||||
|
||||
@@ -185,6 +245,11 @@ class SiloMetadataEditor(QtWidgets.QWidget):
|
||||
header.addWidget(self._lifecycle_combo)
|
||||
outer.addLayout(header)
|
||||
|
||||
# Offline banner
|
||||
self._banner = _make_offline_banner()
|
||||
_update_banner(self._banner)
|
||||
outer.addWidget(self._banner)
|
||||
|
||||
# Separator
|
||||
line = QtWidgets.QFrame()
|
||||
line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
@@ -354,7 +419,7 @@ class SiloMetadataEditor(QtWidgets.QWidget):
|
||||
}
|
||||
|
||||
def _on_save(self):
|
||||
"""Write current form state to obj.RawContent."""
|
||||
"""Write current form state to obj.RawContent and push to server."""
|
||||
new_data = self._collect_data()
|
||||
self._obj.RawContent = json.dumps(new_data, indent=2)
|
||||
self._obj.Proxy.clear_dirty()
|
||||
@@ -362,6 +427,26 @@ class SiloMetadataEditor(QtWidgets.QWidget):
|
||||
self._save_btn.setEnabled(False)
|
||||
self._reset_btn.setEnabled(False)
|
||||
|
||||
# Server write-back (best-effort)
|
||||
pn = _get_part_number()
|
||||
if pn and _is_online():
|
||||
self._push_to_server(pn, new_data)
|
||||
|
||||
def _push_to_server(self, pn, data):
|
||||
"""Push metadata changes to server. Failures are non-fatal."""
|
||||
try:
|
||||
_silo_client.update_metadata(pn, {"fields": data.get("fields", {})})
|
||||
except Exception as exc:
|
||||
FreeCAD.Console.PrintWarning(f"Metadata push failed: {exc}\n")
|
||||
try:
|
||||
_silo_client.patch_lifecycle(pn, data.get("lifecycle", "draft"))
|
||||
except Exception as exc:
|
||||
FreeCAD.Console.PrintWarning(f"Lifecycle push failed: {exc}\n")
|
||||
try:
|
||||
_silo_client.patch_tags(pn, data.get("tags", []))
|
||||
except Exception as exc:
|
||||
FreeCAD.Console.PrintWarning(f"Tags push failed: {exc}\n")
|
||||
|
||||
def _on_reset(self):
|
||||
"""Revert all fields to last-saved state."""
|
||||
# Clear existing field widgets and tags
|
||||
@@ -405,9 +490,88 @@ class SiloMetadataEditor(QtWidgets.QWidget):
|
||||
self._save_btn.setEnabled(False)
|
||||
self._reset_btn.setEnabled(False)
|
||||
|
||||
# -- server integration --------------------------------------------------
|
||||
|
||||
def _rebuild_form(self, data):
|
||||
"""Rebuild lifecycle, tags, and fields from a data dict."""
|
||||
self._schema_name = data.get("schema", self._schema_name)
|
||||
|
||||
# Lifecycle
|
||||
lc = data.get("lifecycle", "draft")
|
||||
idx = self._lifecycle_combo.findText(lc)
|
||||
if idx >= 0:
|
||||
self._lifecycle_combo.blockSignals(True)
|
||||
self._lifecycle_combo.setCurrentIndex(idx)
|
||||
self._lifecycle_combo.blockSignals(False)
|
||||
|
||||
# Tags — clear and rebuild
|
||||
for i in reversed(range(self._tags_layout.count())):
|
||||
w = self._tags_layout.itemAt(i).widget()
|
||||
if w:
|
||||
w.deleteLater()
|
||||
for tag in data.get("tags", []):
|
||||
self._add_tag_chip(tag)
|
||||
|
||||
# Fields — find QGroupBox and rebuild
|
||||
self._field_widgets.clear()
|
||||
for child in self.findChildren(QtWidgets.QGroupBox):
|
||||
if child.title() == "Fields":
|
||||
form = child.layout()
|
||||
while form.count():
|
||||
item = form.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
for key, value in data.get("fields", {}).items():
|
||||
widget = self._make_field_widget(value)
|
||||
self._field_widgets[key] = widget
|
||||
form.addRow(key + ":", widget)
|
||||
break
|
||||
|
||||
def _start_sse(self):
|
||||
if not _is_online():
|
||||
return
|
||||
try:
|
||||
import silo_commands
|
||||
|
||||
self._event_listener = silo_commands.SiloEventListener()
|
||||
self._event_listener.item_updated.connect(self._on_sse_item_updated)
|
||||
self._event_listener.connection_status.connect(self._on_sse_status)
|
||||
self._event_listener.start()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_sse_item_updated(self, part_number):
|
||||
pn = _get_part_number()
|
||||
if not pn or pn != part_number:
|
||||
return
|
||||
if self._obj.Proxy.is_dirty():
|
||||
FreeCAD.Console.PrintWarning(
|
||||
"Metadata: remote update received but local edits exist. "
|
||||
"Save or reset to sync.\n"
|
||||
)
|
||||
return
|
||||
try:
|
||||
server_data = _silo_client.get_metadata(pn)
|
||||
local_data = {
|
||||
"schema": server_data.get("schema_name", self._schema_name),
|
||||
"lifecycle": server_data.get("lifecycle_state", "draft"),
|
||||
"tags": server_data.get("tags", []),
|
||||
"fields": server_data.get("fields", {}),
|
||||
}
|
||||
self._obj.RawContent = json.dumps(local_data, indent=2)
|
||||
self._original_data = copy.deepcopy(local_data)
|
||||
self._rebuild_form(local_data)
|
||||
except Exception as exc:
|
||||
FreeCAD.Console.PrintWarning(f"Metadata SSE refresh failed: {exc}\n")
|
||||
|
||||
def _on_sse_status(self, status, retry_count, error_msg):
|
||||
_update_banner(self._banner)
|
||||
|
||||
# -- close guard ---------------------------------------------------------
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self._event_listener is not None:
|
||||
self._event_listener.stop()
|
||||
if self._obj.Proxy.is_dirty():
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self,
|
||||
@@ -436,14 +600,20 @@ _BADGE_STYLES = {
|
||||
|
||||
|
||||
class SiloHistoryViewer(QtWidgets.QWidget):
|
||||
"""Read-only revision timeline from ``silo/history.json``."""
|
||||
"""Revision timeline from ``silo/history.json`` with live refresh."""
|
||||
|
||||
WINDOW_TITLE = "Silo \u2014 History"
|
||||
|
||||
def __init__(self, obj, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName(f"SiloViewer_{obj.Name}")
|
||||
self._obj = obj
|
||||
self._cards_layout = None
|
||||
self._banner = None
|
||||
self._refresh_btn = None
|
||||
self._event_listener = None
|
||||
self._build_ui(obj.RawContent)
|
||||
self._start_sse()
|
||||
|
||||
def _build_ui(self, raw_content):
|
||||
try:
|
||||
@@ -465,42 +635,112 @@ class SiloHistoryViewer(QtWidgets.QWidget):
|
||||
title.setFont(font)
|
||||
header.addWidget(title)
|
||||
header.addStretch()
|
||||
self._refresh_btn = QtWidgets.QPushButton("Refresh")
|
||||
self._refresh_btn.setEnabled(_is_online())
|
||||
self._refresh_btn.clicked.connect(self._on_refresh)
|
||||
header.addWidget(self._refresh_btn)
|
||||
outer.addLayout(header)
|
||||
|
||||
# Offline banner
|
||||
self._banner = _make_offline_banner()
|
||||
_update_banner(self._banner)
|
||||
outer.addWidget(self._banner)
|
||||
|
||||
# Separator
|
||||
line = QtWidgets.QFrame()
|
||||
line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
outer.addWidget(line)
|
||||
|
||||
if not revisions:
|
||||
placeholder = QtWidgets.QLabel("No revision history available.")
|
||||
placeholder.setAlignment(QtCore.Qt.AlignCenter)
|
||||
outer.addWidget(placeholder)
|
||||
outer.addStretch()
|
||||
return
|
||||
|
||||
# Scroll area with revision cards
|
||||
scroll = QtWidgets.QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
content = QtWidgets.QWidget()
|
||||
cards_layout = QtWidgets.QVBoxLayout(content)
|
||||
cards_layout.setContentsMargins(0, 0, 0, 0)
|
||||
cards_layout.setSpacing(0)
|
||||
self._cards_layout = QtWidgets.QVBoxLayout(content)
|
||||
self._cards_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._cards_layout.setSpacing(0)
|
||||
|
||||
if not revisions:
|
||||
placeholder = QtWidgets.QLabel("No revision history available.")
|
||||
placeholder.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self._cards_layout.addWidget(placeholder)
|
||||
|
||||
for i, rev in enumerate(revisions):
|
||||
cards_layout.addWidget(self._make_revision_card(rev))
|
||||
self._cards_layout.addWidget(self._make_revision_card(rev))
|
||||
if i < len(revisions) - 1:
|
||||
sep = QtWidgets.QFrame()
|
||||
sep.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
sep.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
cards_layout.addWidget(sep)
|
||||
self._cards_layout.addWidget(sep)
|
||||
|
||||
cards_layout.addStretch()
|
||||
self._cards_layout.addStretch()
|
||||
scroll.setWidget(content)
|
||||
outer.addWidget(scroll, 1)
|
||||
|
||||
# -- server integration --------------------------------------------------
|
||||
|
||||
def _on_refresh(self):
|
||||
pn = _get_part_number()
|
||||
if not pn or not _is_online():
|
||||
return
|
||||
try:
|
||||
revisions = _silo_client.get_revisions(pn)
|
||||
except Exception as exc:
|
||||
FreeCAD.Console.PrintWarning(f"History refresh failed: {exc}\n")
|
||||
return
|
||||
data = {"revisions": revisions}
|
||||
self._obj.RawContent = json.dumps(data, indent=2)
|
||||
self._obj.Proxy.mark_dirty()
|
||||
self._rebuild_cards(revisions)
|
||||
|
||||
def _rebuild_cards(self, revisions):
|
||||
"""Clear and rebuild revision cards."""
|
||||
while self._cards_layout.count():
|
||||
item = self._cards_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
if not revisions:
|
||||
placeholder = QtWidgets.QLabel("No revision history available.")
|
||||
placeholder.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self._cards_layout.addWidget(placeholder)
|
||||
else:
|
||||
for i, rev in enumerate(revisions):
|
||||
self._cards_layout.addWidget(self._make_revision_card(rev))
|
||||
if i < len(revisions) - 1:
|
||||
sep = QtWidgets.QFrame()
|
||||
sep.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
sep.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
self._cards_layout.addWidget(sep)
|
||||
self._cards_layout.addStretch()
|
||||
|
||||
def _start_sse(self):
|
||||
if not _is_online():
|
||||
return
|
||||
try:
|
||||
import silo_commands
|
||||
|
||||
self._event_listener = silo_commands.SiloEventListener()
|
||||
self._event_listener.revision_created.connect(self._on_sse_revision)
|
||||
self._event_listener.connection_status.connect(self._on_sse_status)
|
||||
self._event_listener.start()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_sse_revision(self, part_number, revision):
|
||||
pn = _get_part_number()
|
||||
if pn and pn == part_number:
|
||||
self._on_refresh()
|
||||
|
||||
def _on_sse_status(self, status, retry_count, error_msg):
|
||||
_update_banner(self._banner)
|
||||
self._refresh_btn.setEnabled(_is_online())
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self._event_listener is not None:
|
||||
self._event_listener.stop()
|
||||
event.accept()
|
||||
|
||||
def _make_revision_card(self, rev):
|
||||
"""Build a widget for a single revision entry."""
|
||||
card = QtWidgets.QWidget()
|
||||
@@ -697,13 +937,18 @@ def _open_url(url):
|
||||
|
||||
|
||||
class SiloDependencyTable(QtWidgets.QWidget):
|
||||
"""Table view of assembly dependencies from ``silo/dependencies.json``."""
|
||||
"""Table view of assembly dependencies with server resolution."""
|
||||
|
||||
WINDOW_TITLE = "Silo \u2014 Dependencies"
|
||||
|
||||
def __init__(self, obj, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName(f"SiloViewer_{obj.Name}")
|
||||
self._obj = obj
|
||||
self._links = []
|
||||
self._model = None
|
||||
self._table = None
|
||||
self._banner = None
|
||||
self._build_ui(obj.RawContent)
|
||||
|
||||
def _build_ui(self, raw_content):
|
||||
@@ -711,65 +956,147 @@ class SiloDependencyTable(QtWidgets.QWidget):
|
||||
data = json.loads(raw_content)
|
||||
except Exception:
|
||||
data = {}
|
||||
links = data.get("links", [])
|
||||
self._links = data.get("links", [])
|
||||
|
||||
outer = QtWidgets.QVBoxLayout(self)
|
||||
outer.setContentsMargins(16, 16, 16, 16)
|
||||
outer.setSpacing(12)
|
||||
|
||||
# Header
|
||||
# Header with buttons
|
||||
header = QtWidgets.QHBoxLayout()
|
||||
title = QtWidgets.QLabel("Assembly Dependencies")
|
||||
font = title.font()
|
||||
font.setPointSize(font.pointSize() + 2)
|
||||
font.setBold(True)
|
||||
title.setFont(font)
|
||||
outer.addWidget(title)
|
||||
header.addWidget(title)
|
||||
header.addStretch()
|
||||
|
||||
self._refresh_btn = QtWidgets.QPushButton("Refresh")
|
||||
self._refresh_btn.setEnabled(_is_online())
|
||||
self._refresh_btn.clicked.connect(self._on_refresh)
|
||||
header.addWidget(self._refresh_btn)
|
||||
|
||||
self._download_btn = QtWidgets.QPushButton("Download")
|
||||
self._download_btn.setEnabled(_is_online())
|
||||
self._download_btn.setToolTip("Download selected unresolved dependencies")
|
||||
self._download_btn.clicked.connect(self._on_download)
|
||||
header.addWidget(self._download_btn)
|
||||
outer.addLayout(header)
|
||||
|
||||
# Offline banner
|
||||
self._banner = _make_offline_banner()
|
||||
_update_banner(self._banner)
|
||||
outer.addWidget(self._banner)
|
||||
|
||||
sep = QtWidgets.QFrame()
|
||||
sep.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
sep.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
outer.addWidget(sep)
|
||||
|
||||
if not links:
|
||||
if not self._links:
|
||||
outer.addWidget(QtWidgets.QLabel("No dependencies defined."))
|
||||
outer.addStretch()
|
||||
return
|
||||
|
||||
# Table
|
||||
from PySide.QtGui import QStandardItemModel, QStandardItem
|
||||
from PySide.QtGui import QStandardItem, QStandardItemModel
|
||||
|
||||
table = QtWidgets.QTableView()
|
||||
model = QStandardItemModel(len(links), 5)
|
||||
model.setHorizontalHeaderLabels(["Label", "Part Number", "Rev", "Qty", "Local"])
|
||||
self._table = QtWidgets.QTableView()
|
||||
self._model = QStandardItemModel(len(self._links), 5)
|
||||
self._model.setHorizontalHeaderLabels(
|
||||
["Label", "Part Number", "Rev", "Qty", "Status"]
|
||||
)
|
||||
|
||||
for row, link in enumerate(links):
|
||||
# Try server-side resolution first
|
||||
server_map = self._resolve_via_server()
|
||||
|
||||
for row, link in enumerate(self._links):
|
||||
label_item = QStandardItem(link.get("label", ""))
|
||||
label_item.setEditable(False)
|
||||
model.setItem(row, 0, label_item)
|
||||
self._model.setItem(row, 0, label_item)
|
||||
|
||||
pn_item = QStandardItem(link.get("part_number", ""))
|
||||
pn_item.setEditable(False)
|
||||
model.setItem(row, 1, pn_item)
|
||||
self._model.setItem(row, 1, pn_item)
|
||||
|
||||
rev_item = QStandardItem(str(link.get("revision", "")))
|
||||
rev_item.setEditable(False)
|
||||
model.setItem(row, 2, rev_item)
|
||||
self._model.setItem(row, 2, rev_item)
|
||||
|
||||
qty_item = QStandardItem(str(link.get("quantity", "")))
|
||||
qty_item.setEditable(False)
|
||||
model.setItem(row, 3, qty_item)
|
||||
self._model.setItem(row, 3, qty_item)
|
||||
|
||||
# Resolution: check if UUID is in any open document
|
||||
resolved = _is_dependency_resolved(link.get("uuid", ""))
|
||||
status_item = QStandardItem("\u2713" if resolved else "\u2717")
|
||||
uuid = link.get("uuid", "")
|
||||
icon = self._status_icon(uuid, server_map)
|
||||
status_item = QStandardItem(icon)
|
||||
status_item.setEditable(False)
|
||||
model.setItem(row, 4, status_item)
|
||||
self._model.setItem(row, 4, status_item)
|
||||
|
||||
table.setModel(model)
|
||||
table.horizontalHeader().setStretchLastSection(True)
|
||||
table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
outer.addWidget(table, 1)
|
||||
self._table.setModel(self._model)
|
||||
self._table.horizontalHeader().setStretchLastSection(True)
|
||||
self._table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
self._table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
outer.addWidget(self._table, 1)
|
||||
|
||||
# -- server integration --------------------------------------------------
|
||||
|
||||
def _resolve_via_server(self):
|
||||
"""Use server-side resolution if online, else return None."""
|
||||
pn = _get_part_number()
|
||||
if pn and _is_online():
|
||||
try:
|
||||
resolved = _silo_client.resolve_dependencies(pn)
|
||||
return {r["uuid"]: r for r in resolved}
|
||||
except Exception as exc:
|
||||
FreeCAD.Console.PrintWarning(f"Dependency resolve failed: {exc}\n")
|
||||
return None
|
||||
|
||||
def _status_icon(self, uuid, server_map):
|
||||
"""Return status icon for a dependency UUID."""
|
||||
if server_map and uuid in server_map:
|
||||
r = server_map[uuid]
|
||||
if r.get("resolved", False):
|
||||
return "\u2713" # check mark
|
||||
if r.get("file_available", False):
|
||||
return "\u2193" # down arrow — downloadable
|
||||
return "\u2717" # cross mark
|
||||
return "\u2713" if _is_dependency_resolved(uuid) else "\u2717"
|
||||
|
||||
def _on_refresh(self):
|
||||
"""Re-resolve dependencies and update status column."""
|
||||
if self._model is None:
|
||||
return
|
||||
server_map = self._resolve_via_server()
|
||||
for row, link in enumerate(self._links):
|
||||
uuid = link.get("uuid", "")
|
||||
icon = self._status_icon(uuid, server_map)
|
||||
item = self._model.item(row, 4)
|
||||
if item:
|
||||
item.setText(icon)
|
||||
|
||||
def _on_download(self):
|
||||
"""Download selected unresolved dependencies."""
|
||||
if self._table is None:
|
||||
return
|
||||
indexes = self._table.selectionModel().selectedRows()
|
||||
if not indexes:
|
||||
return
|
||||
for idx in indexes:
|
||||
pn_item = self._model.item(idx.row(), 1)
|
||||
pn = pn_item.text() if pn_item else ""
|
||||
if not pn:
|
||||
continue
|
||||
try:
|
||||
import silo_commands
|
||||
|
||||
path = silo_commands._sync.download_file(pn)
|
||||
if path:
|
||||
FreeCAD.Console.PrintMessage(f"Downloaded: {path}\n")
|
||||
except Exception as exc:
|
||||
FreeCAD.Console.PrintWarning(f"Download {pn} failed: {exc}\n")
|
||||
self._on_refresh()
|
||||
|
||||
|
||||
def _is_dependency_resolved(uuid_str):
|
||||
|
||||
Reference in New Issue
Block a user