diff --git a/src/Mod/Create/InitGui.py b/src/Mod/Create/InitGui.py index 90cbc47ce6..0caf77f0ab 100644 --- a/src/Mod/Create/InitGui.py +++ b/src/Mod/Create/InitGui.py @@ -95,8 +95,278 @@ def _setup_silo_menu(): FreeCAD.Console.PrintLog(f"Create: Silo menu setup skipped: {e}\n") +class SiloActivityPanel: + """Interactive database browser with search, item table, and details pane.""" + + def __init__(self): + from PySide import QtCore, QtGui, QtWidgets + + self._QtCore = QtCore + self._QtWidgets = QtWidgets + + import silo_commands + + self._client = silo_commands._client + self._SiloSync = silo_commands.SiloSync + + self.widget = QtWidgets.QWidget() + self._items_data = [] + self._debounce_timer = QtCore.QTimer() + self._debounce_timer.setSingleShot(True) + self._debounce_timer.setInterval(300) + self._debounce_timer.timeout.connect(self._do_refresh) + + self._build_ui() + self._do_refresh() + + def _build_ui(self): + QtWidgets = self._QtWidgets + QtCore = self._QtCore + + layout = QtWidgets.QVBoxLayout(self.widget) + layout.setContentsMargins(4, 4, 4, 4) + layout.setSpacing(4) + + # --- Search / filter toolbar --- + toolbar = QtWidgets.QHBoxLayout() + toolbar.setSpacing(4) + + self._search = QtWidgets.QLineEdit() + self._search.setPlaceholderText("Search parts...") + self._search.setClearButtonEnabled(True) + self._search.textChanged.connect(self._on_search_changed) + toolbar.addWidget(self._search, 1) + + self._type_filter = QtWidgets.QComboBox() + self._type_filter.addItem("All", "") + self._type_filter.addItem("Part", "part") + self._type_filter.addItem("Assembly", "assembly") + self._type_filter.setFixedWidth(90) + self._type_filter.currentIndexChanged.connect(self._on_search_changed) + toolbar.addWidget(self._type_filter) + + refresh_btn = QtWidgets.QPushButton("Refresh") + refresh_btn.setFixedWidth(60) + refresh_btn.clicked.connect(self._do_refresh) + toolbar.addWidget(refresh_btn) + + layout.addLayout(toolbar) + + # --- Item table --- + self._table = QtWidgets.QTableWidget() + self._table.setColumnCount(4) + self._table.setHorizontalHeaderLabels( + ["Part Number", "Description", "Type", "Updated"] + ) + header = self._table.horizontalHeader() + header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) + header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) + header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents) + self._table.verticalHeader().setVisible(False) + self._table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self._table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + self._table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self._table.itemSelectionChanged.connect(self._on_selection_changed) + self._table.doubleClicked.connect(self._on_open) + layout.addWidget(self._table, 1) + + # --- Details pane --- + self._details_group = QtWidgets.QGroupBox("Part Details") + details_layout = QtWidgets.QVBoxLayout(self._details_group) + details_layout.setSpacing(2) + + form = QtWidgets.QFormLayout() + form.setHorizontalSpacing(12) + self._detail_pn = QtWidgets.QLabel("-") + self._detail_pn.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + self._detail_desc = QtWidgets.QLabel("-") + self._detail_desc.setWordWrap(True) + self._detail_desc.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + self._detail_type = QtWidgets.QLabel("-") + self._detail_rev = QtWidgets.QLabel("-") + self._detail_updated = QtWidgets.QLabel("-") + self._detail_projects = QtWidgets.QLabel("-") + self._detail_projects.setWordWrap(True) + form.addRow("Part Number:", self._detail_pn) + form.addRow("Description:", self._detail_desc) + form.addRow("Type:", self._detail_type) + form.addRow("Revision:", self._detail_rev) + form.addRow("Updated:", self._detail_updated) + form.addRow("Projects:", self._detail_projects) + details_layout.addLayout(form) + + btn_row = QtWidgets.QHBoxLayout() + self._open_btn = QtWidgets.QPushButton("Open") + self._open_btn.clicked.connect(self._on_open) + self._info_btn = QtWidgets.QPushButton("Info...") + self._info_btn.clicked.connect(self._on_info) + btn_row.addWidget(self._open_btn) + btn_row.addStretch() + btn_row.addWidget(self._info_btn) + details_layout.addLayout(btn_row) + + self._details_group.setVisible(False) + layout.addWidget(self._details_group) + + # --- Actions --- + + def _on_search_changed(self): + self._debounce_timer.start() + + def _do_refresh(self): + search = self._search.text().strip() + item_type = self._type_filter.currentData() or "" + try: + items = self._client.list_items(search=search, item_type=item_type) + if not isinstance(items, list): + items = [] + except Exception: + items = None + + self._items_data = items if items is not None else [] + self._populate_table(items) + + def _populate_table(self, items): + QtWidgets = self._QtWidgets + self._table.setRowCount(0) + self._details_group.setVisible(False) + + if items is None: + self._table.setRowCount(1) + self._table.setSpan(0, 0, 1, 4) + msg = QtWidgets.QTableWidgetItem("(Unable to connect to Silo database)") + msg.setForeground( + self._table.palette().color( + self._table.palette().Disabled, self._table.palette().Text + ) + ) + self._table.setItem(0, 0, msg) + return + + if not items: + self._table.setRowCount(1) + self._table.setSpan(0, 0, 1, 4) + msg = QtWidgets.QTableWidgetItem("(No items found)") + msg.setForeground( + self._table.palette().color( + self._table.palette().Disabled, self._table.palette().Text + ) + ) + self._table.setItem(0, 0, msg) + return + + self._table.setRowCount(len(items)) + for i, item in enumerate(items): + pn = item.get("part_number", "") + desc = item.get("description", "") + itype = item.get("item_type", "") + updated = item.get("updated_at", "")[:10] if item.get("updated_at") else "" + + self._table.setItem(i, 0, QtWidgets.QTableWidgetItem(pn)) + self._table.setItem(i, 1, QtWidgets.QTableWidgetItem(desc)) + self._table.setItem(i, 2, QtWidgets.QTableWidgetItem(itype)) + self._table.setItem(i, 3, QtWidgets.QTableWidgetItem(updated)) + + def _selected_item(self): + rows = self._table.selectionModel().selectedRows() + if not rows: + return None + row = rows[0].row() + if row < len(self._items_data): + return self._items_data[row] + return None + + def _on_selection_changed(self): + item = self._selected_item() + if not item: + self._details_group.setVisible(False) + return + + self._detail_pn.setText(item.get("part_number", "-")) + self._detail_desc.setText(item.get("description", "-") or "-") + self._detail_type.setText(item.get("item_type", "-")) + self._detail_rev.setText(str(item.get("current_revision", "-"))) + updated = item.get("updated_at", "") + self._detail_updated.setText(updated[:10] if updated else "-") + + # Fetch projects in background-safe way (quick API call) + pn = item.get("part_number", "") + try: + projects = self._client.get_item_projects(pn) + codes = [p.get("code", "") for p in projects if p.get("code")] + self._detail_projects.setText(", ".join(codes) if codes else "-") + except Exception: + self._detail_projects.setText("-") + + self._details_group.setVisible(True) + + def _on_open(self): + item = self._selected_item() + if not item: + return + pn = item.get("part_number", "") + if not pn: + return + try: + sync = self._SiloSync(self._client) + sync.open_item(pn) + except Exception as e: + FreeCAD.Console.PrintError(f"Failed to open {pn}: {e}\n") + + def _on_info(self): + item = self._selected_item() + if not item: + return + pn = item.get("part_number", "") + if not pn: + return + try: + from PySide import QtGui + + item_data = self._client.get_item(pn) + revisions = self._client.get_revisions(pn) + + try: + projects = self._client.get_item_projects(pn) + project_codes = [p.get("code", "") for p in projects if p.get("code")] + except Exception: + project_codes = [] + + msg = f"
Type: {item_data.get('item_type', '-')}
" + msg += f"Description: {item_data.get('description', '-')}
" + msg += f"Projects: {', '.join(project_codes) if project_codes else 'None'}
" + msg += f"Current Revision: {item_data.get('current_revision', 1)}
" + + has_file, _ = self._client.has_file(pn) + msg += f"File in MinIO: {'Yes' if has_file else 'No'}
" + + if revisions: + current_status = revisions[0].get("status", "draft") + msg += f"Current Status: {current_status}
" + + msg += "| Rev | Status | Date | File | Comment |
|---|---|---|---|---|
| {rev['revision_number']} | {status} | {date} | {file_icon} | {comment} |