Add revision control and project tagging migration
This commit is contained in:
@@ -18,6 +18,190 @@ SILO_PROJECTS_DIR = os.environ.get(
|
||||
"SILO_PROJECTS_DIR", os.path.expanduser("~/projects")
|
||||
)
|
||||
|
||||
# Category name mapping for folder structure
|
||||
# Format: CCC -> "descriptive_name"
|
||||
CATEGORY_NAMES = {
|
||||
# Fasteners
|
||||
"F01": "screws_bolts",
|
||||
"F02": "threaded_rods",
|
||||
"F03": "eyebolts",
|
||||
"F04": "u_bolts",
|
||||
"F05": "nuts",
|
||||
"F06": "washers",
|
||||
"F07": "shims",
|
||||
"F08": "inserts",
|
||||
"F09": "spacers",
|
||||
"F10": "pins",
|
||||
"F11": "anchors",
|
||||
"F12": "nails",
|
||||
"F13": "rivets",
|
||||
"F14": "staples",
|
||||
"F15": "key_stock",
|
||||
"F16": "retaining_rings",
|
||||
"F17": "cable_ties",
|
||||
"F18": "hook_loop",
|
||||
# Fluid Fittings
|
||||
"C01": "full_couplings",
|
||||
"C02": "half_couplings",
|
||||
"C03": "reducers",
|
||||
"C04": "elbows",
|
||||
"C05": "tees",
|
||||
"C06": "crosses",
|
||||
"C07": "unions",
|
||||
"C08": "adapters",
|
||||
"C09": "plugs_caps",
|
||||
"C10": "nipples",
|
||||
"C11": "flanges",
|
||||
"C12": "valves",
|
||||
"C13": "quick_disconnects",
|
||||
"C14": "hose_barbs",
|
||||
"C15": "compression_fittings",
|
||||
"C16": "tubing",
|
||||
"C17": "hoses",
|
||||
# Motion Components
|
||||
"R01": "ball_bearings",
|
||||
"R02": "roller_bearings",
|
||||
"R03": "sleeve_bearings",
|
||||
"R04": "thrust_bearings",
|
||||
"R05": "linear_bearings",
|
||||
"R06": "spur_gears",
|
||||
"R07": "helical_gears",
|
||||
"R08": "bevel_gears",
|
||||
"R09": "worm_gears",
|
||||
"R10": "rack_pinion",
|
||||
"R11": "sprockets",
|
||||
"R12": "timing_pulleys",
|
||||
"R13": "v_belt_pulleys",
|
||||
"R14": "idler_pulleys",
|
||||
"R15": "wheels",
|
||||
"R16": "casters",
|
||||
"R17": "shaft_couplings",
|
||||
"R18": "clutches",
|
||||
"R19": "brakes",
|
||||
"R20": "lead_screws",
|
||||
"R21": "ball_screws",
|
||||
"R22": "linear_rails",
|
||||
"R23": "linear_actuators",
|
||||
"R24": "brushed_dc_motor",
|
||||
"R25": "brushless_dc_motor",
|
||||
"R26": "stepper_motor",
|
||||
"R27": "servo_motor",
|
||||
"R28": "ac_induction_motor",
|
||||
"R29": "gear_motor",
|
||||
"R30": "motor_driver",
|
||||
"R31": "motor_controller",
|
||||
"R32": "encoder",
|
||||
"R33": "pneumatic_cylinder",
|
||||
"R34": "pneumatic_actuator",
|
||||
"R35": "pneumatic_valve",
|
||||
"R36": "pneumatic_regulator",
|
||||
"R37": "pneumatic_frl_unit",
|
||||
"R38": "air_compressor",
|
||||
"R39": "vacuum_pump",
|
||||
"R40": "hydraulic_cylinder",
|
||||
"R41": "hydraulic_pump",
|
||||
"R42": "hydraulic_motor",
|
||||
"R43": "hydraulic_valve",
|
||||
"R44": "hydraulic_accumulator",
|
||||
# Structural Materials
|
||||
"S01": "square_tube",
|
||||
"S02": "round_tube",
|
||||
"S03": "rectangular_tube",
|
||||
"S04": "i_beam",
|
||||
"S05": "t_slot_extrusion",
|
||||
"S06": "angle",
|
||||
"S07": "channel",
|
||||
"S08": "flat_bar",
|
||||
"S09": "round_bar",
|
||||
"S10": "square_bar",
|
||||
"S11": "hex_bar",
|
||||
"S12": "sheet_metal",
|
||||
"S13": "plate",
|
||||
"S14": "expanded_metal",
|
||||
"S15": "perforated_sheet",
|
||||
"S16": "wire_mesh",
|
||||
"S17": "grating",
|
||||
# Electrical Components
|
||||
"E01": "wire",
|
||||
"E02": "cable",
|
||||
"E03": "connectors",
|
||||
"E04": "terminals",
|
||||
"E05": "circuit_breakers",
|
||||
"E06": "fuses",
|
||||
"E07": "relays",
|
||||
"E08": "contactors",
|
||||
"E09": "switches",
|
||||
"E10": "buttons",
|
||||
"E11": "indicators",
|
||||
"E12": "resistors",
|
||||
"E13": "capacitors",
|
||||
"E14": "inductors",
|
||||
"E15": "transformers",
|
||||
"E16": "diodes",
|
||||
"E17": "transistors",
|
||||
"E18": "ics",
|
||||
"E19": "microcontrollers",
|
||||
"E20": "sensors",
|
||||
"E21": "displays",
|
||||
"E22": "power_supplies",
|
||||
"E23": "batteries",
|
||||
"E24": "pcb",
|
||||
"E25": "enclosures",
|
||||
"E26": "heat_sinks",
|
||||
"E27": "fans",
|
||||
# Mechanical Components
|
||||
"M01": "compression_springs",
|
||||
"M02": "extension_springs",
|
||||
"M03": "torsion_springs",
|
||||
"M04": "gas_springs",
|
||||
"M05": "dampers",
|
||||
"M06": "shock_absorbers",
|
||||
"M07": "vibration_mounts",
|
||||
"M08": "hinges",
|
||||
"M09": "latches",
|
||||
"M10": "handles",
|
||||
"M11": "knobs",
|
||||
"M12": "levers",
|
||||
"M13": "linkages",
|
||||
"M14": "cams",
|
||||
"M15": "bellows",
|
||||
"M16": "seals",
|
||||
"M17": "o_rings",
|
||||
"M18": "gaskets",
|
||||
# Tooling and Fixtures
|
||||
"T01": "jigs",
|
||||
"T02": "fixtures",
|
||||
"T03": "molds",
|
||||
"T04": "dies",
|
||||
"T05": "gauges",
|
||||
"T06": "templates",
|
||||
"T07": "work_holding",
|
||||
"T08": "test_fixtures",
|
||||
# Assemblies
|
||||
"A01": "mechanical_assembly",
|
||||
"A02": "electrical_assembly",
|
||||
"A03": "electromechanical_assembly",
|
||||
"A04": "subassembly",
|
||||
"A05": "cable_assembly",
|
||||
"A06": "pneumatic_assembly",
|
||||
"A07": "hydraulic_assembly",
|
||||
# Purchased/Off-the-Shelf
|
||||
"P01": "purchased_mechanical",
|
||||
"P02": "purchased_electrical",
|
||||
"P03": "purchased_assembly",
|
||||
"P04": "raw_material",
|
||||
"P05": "consumables",
|
||||
# Custom Fabricated Parts
|
||||
"X01": "machined_part",
|
||||
"X02": "sheet_metal_part",
|
||||
"X03": "3d_printed_part",
|
||||
"X04": "cast_part",
|
||||
"X05": "molded_part",
|
||||
"X06": "welded_fabrication",
|
||||
"X07": "laser_cut_part",
|
||||
"X08": "waterjet_cut_part",
|
||||
}
|
||||
|
||||
|
||||
# Icon directory
|
||||
def _get_icon_dir():
|
||||
@@ -169,18 +353,21 @@ class SiloClient:
|
||||
return self._request("GET", "/items?" + "&".join(params))
|
||||
|
||||
def create_item(
|
||||
self, schema: str, project: str, category: str, description: str = ""
|
||||
self,
|
||||
schema: str,
|
||||
category: str,
|
||||
description: str = "",
|
||||
projects: List[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
return self._request(
|
||||
"POST",
|
||||
"/items",
|
||||
{
|
||||
"schema": schema,
|
||||
"project": project,
|
||||
"category": category,
|
||||
"description": description,
|
||||
},
|
||||
)
|
||||
"""Create a new item with optional project tags."""
|
||||
data = {
|
||||
"schema": schema,
|
||||
"category": category,
|
||||
"description": description,
|
||||
}
|
||||
if projects:
|
||||
data["projects"] = projects
|
||||
return self._request("POST", "/items", data)
|
||||
|
||||
def update_item(
|
||||
self, part_number: str, description: str = None, item_type: str = None
|
||||
@@ -199,8 +386,21 @@ class SiloClient:
|
||||
return self._request("GET", f"/schemas/{name}")
|
||||
|
||||
def get_projects(self) -> list:
|
||||
"""Get list of all projects."""
|
||||
return self._request("GET", "/projects")
|
||||
|
||||
def get_item_projects(self, part_number: str) -> list:
|
||||
"""Get projects associated with an item."""
|
||||
return self._request("GET", f"/items/{part_number}/projects")
|
||||
|
||||
def add_item_projects(
|
||||
self, part_number: str, project_codes: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Add project tags to an item."""
|
||||
return self._request(
|
||||
"POST", f"/items/{part_number}/projects", {"projects": project_codes}
|
||||
)
|
||||
|
||||
def has_file(self, part_number: str) -> Tuple[bool, Optional[int]]:
|
||||
"""Check if item has files in MinIO."""
|
||||
try:
|
||||
@@ -212,6 +412,39 @@ class SiloClient:
|
||||
except Exception:
|
||||
return False, None
|
||||
|
||||
def compare_revisions(
|
||||
self, part_number: str, from_rev: int, to_rev: int
|
||||
) -> Dict[str, Any]:
|
||||
"""Compare two revisions and return differences."""
|
||||
return self._request(
|
||||
"GET",
|
||||
f"/items/{part_number}/revisions/compare?from={from_rev}&to={to_rev}",
|
||||
)
|
||||
|
||||
def rollback_revision(
|
||||
self, part_number: str, revision: int, comment: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new revision by rolling back to a previous one."""
|
||||
data = {}
|
||||
if comment:
|
||||
data["comment"] = comment
|
||||
return self._request(
|
||||
"POST", f"/items/{part_number}/revisions/{revision}/rollback", data
|
||||
)
|
||||
|
||||
def update_revision(
|
||||
self, part_number: str, revision: int, status: str = None, labels: list = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Update revision status and/or labels."""
|
||||
data = {}
|
||||
if status:
|
||||
data["status"] = status
|
||||
if labels is not None:
|
||||
data["labels"] = labels
|
||||
return self._request(
|
||||
"PATCH", f"/items/{part_number}/revisions/{revision}", data
|
||||
)
|
||||
|
||||
|
||||
_client = SiloClient()
|
||||
|
||||
@@ -227,54 +460,86 @@ def sanitize_filename(name: str) -> str:
|
||||
return sanitized[:50]
|
||||
|
||||
|
||||
def parse_part_number(part_number: str) -> Tuple[str, str, str]:
|
||||
"""Parse part number into (project, category, sequence)."""
|
||||
def parse_part_number(part_number: str) -> Tuple[str, str]:
|
||||
"""Parse part number into (category, sequence).
|
||||
|
||||
New format: CCC-NNNN (e.g., F01-0001)
|
||||
Returns: (category_code, sequence)
|
||||
"""
|
||||
parts = part_number.split("-")
|
||||
if len(parts) >= 3:
|
||||
return parts[0], parts[1], parts[2]
|
||||
return part_number, "", ""
|
||||
if len(parts) >= 2:
|
||||
return parts[0], parts[1]
|
||||
return part_number, ""
|
||||
|
||||
|
||||
def get_category_folder_name(category_code: str) -> str:
|
||||
"""Get the folder name for a category (e.g., 'F01_screws_bolts')."""
|
||||
name = CATEGORY_NAMES.get(category_code.upper(), "misc")
|
||||
return f"{category_code}_{name}"
|
||||
|
||||
|
||||
def get_cad_file_path(part_number: str, description: str = "") -> Path:
|
||||
"""Generate canonical file path for a CAD file."""
|
||||
project_code, _, _ = parse_part_number(part_number)
|
||||
"""Generate canonical file path for a CAD file.
|
||||
|
||||
Path format: ~/projects/cad/{category_code}_{category_name}/{part_number}_{description}.FCStd
|
||||
Example: ~/projects/cad/F01_screws_bolts/F01-0001_M3_Socket_Screw.FCStd
|
||||
"""
|
||||
category, _ = parse_part_number(part_number)
|
||||
folder_name = get_category_folder_name(category)
|
||||
|
||||
if description:
|
||||
filename = f"{part_number}_{sanitize_filename(description)}.FCStd"
|
||||
else:
|
||||
filename = f"{part_number}.FCStd"
|
||||
return get_projects_dir() / project_code.lower() / "cad" / filename
|
||||
|
||||
return get_projects_dir() / "cad" / folder_name / filename
|
||||
|
||||
|
||||
def find_file_by_part_number(part_number: str) -> Optional[Path]:
|
||||
"""Find existing CAD file for a part number."""
|
||||
project_code, _, _ = parse_part_number(part_number)
|
||||
cad_dir = get_projects_dir() / project_code.lower() / "cad"
|
||||
if not cad_dir.exists():
|
||||
return None
|
||||
matches = list(cad_dir.glob(f"{part_number}*.FCStd"))
|
||||
return matches[0] if matches else None
|
||||
category, _ = parse_part_number(part_number)
|
||||
folder_name = get_category_folder_name(category)
|
||||
cad_dir = get_projects_dir() / "cad" / folder_name
|
||||
|
||||
if cad_dir.exists():
|
||||
matches = list(cad_dir.glob(f"{part_number}*.FCStd"))
|
||||
if matches:
|
||||
return matches[0]
|
||||
|
||||
# Also search in base cad directory (for older files or different structures)
|
||||
base_cad_dir = get_projects_dir() / "cad"
|
||||
if base_cad_dir.exists():
|
||||
# Search all subdirectories
|
||||
for subdir in base_cad_dir.iterdir():
|
||||
if subdir.is_dir():
|
||||
matches = list(subdir.glob(f"{part_number}*.FCStd"))
|
||||
if matches:
|
||||
return matches[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def search_local_files(search_term: str = "", project_filter: str = "") -> list:
|
||||
"""Search for CAD files in local projects directory."""
|
||||
def search_local_files(search_term: str = "", category_filter: str = "") -> list:
|
||||
"""Search for CAD files in local cad directory."""
|
||||
results = []
|
||||
base_dir = get_projects_dir()
|
||||
if not base_dir.exists():
|
||||
cad_dir = get_projects_dir() / "cad"
|
||||
if not cad_dir.exists():
|
||||
return results
|
||||
|
||||
search_lower = search_term.lower()
|
||||
|
||||
for project_dir in base_dir.iterdir():
|
||||
if not project_dir.is_dir():
|
||||
continue
|
||||
if project_filter and project_dir.name.lower() != project_filter.lower():
|
||||
for category_dir in cad_dir.iterdir():
|
||||
if not category_dir.is_dir():
|
||||
continue
|
||||
|
||||
cad_dir = project_dir / "cad"
|
||||
if not cad_dir.exists():
|
||||
# Extract category code from folder name (e.g., "F01_screws_bolts" -> "F01")
|
||||
folder_name = category_dir.name
|
||||
category_code = folder_name.split("_")[0] if "_" in folder_name else folder_name
|
||||
|
||||
if category_filter and category_code.upper() != category_filter.upper():
|
||||
continue
|
||||
|
||||
for fcstd_file in cad_dir.glob("*.FCStd"):
|
||||
for fcstd_file in category_dir.glob("*.FCStd"):
|
||||
filename = fcstd_file.stem
|
||||
parts = filename.split("_", 1)
|
||||
part_number = parts[0]
|
||||
@@ -298,7 +563,7 @@ def search_local_files(search_term: str = "", project_filter: str = "") -> list:
|
||||
"path": str(fcstd_file),
|
||||
"part_number": part_number,
|
||||
"description": description,
|
||||
"project": project_dir.name.upper(),
|
||||
"category": category_code,
|
||||
"modified": modified,
|
||||
"source": "local",
|
||||
}
|
||||
@@ -726,27 +991,7 @@ class Silo_New:
|
||||
|
||||
sel = FreeCADGui.Selection.getSelection()
|
||||
|
||||
# Project code
|
||||
try:
|
||||
projects = _client.get_projects()
|
||||
if projects:
|
||||
project, ok = QtGui.QInputDialog.getItem(
|
||||
None, "New Item", "Project:", projects, 0, True
|
||||
)
|
||||
else:
|
||||
project, ok = QtGui.QInputDialog.getText(
|
||||
None, "New Item", "Project code (5 chars):"
|
||||
)
|
||||
except Exception:
|
||||
project, ok = QtGui.QInputDialog.getText(
|
||||
None, "New Item", "Project code (5 chars):"
|
||||
)
|
||||
|
||||
if not ok or not project:
|
||||
return
|
||||
project = project.upper().strip()[:5]
|
||||
|
||||
# Category
|
||||
# Category selection
|
||||
try:
|
||||
schema = _client.get_schema()
|
||||
categories = schema.get("segments", [])
|
||||
@@ -784,8 +1029,52 @@ class Silo_New:
|
||||
if not ok:
|
||||
return
|
||||
|
||||
# Optional project tagging
|
||||
selected_projects = []
|
||||
try:
|
||||
result = _client.create_item("kindred-rd", project, category, description)
|
||||
projects = _client.get_projects()
|
||||
if projects:
|
||||
project_codes = [p.get("code", "") for p in projects if p.get("code")]
|
||||
if project_codes:
|
||||
# Multi-select dialog for projects
|
||||
dialog = QtGui.QDialog()
|
||||
dialog.setWindowTitle("Tag with Projects (Optional)")
|
||||
dialog.setMinimumWidth(300)
|
||||
layout = QtGui.QVBoxLayout(dialog)
|
||||
|
||||
label = QtGui.QLabel("Select projects to tag this item with:")
|
||||
layout.addWidget(label)
|
||||
|
||||
list_widget = QtGui.QListWidget()
|
||||
list_widget.setSelectionMode(QtGui.QAbstractItemView.MultiSelection)
|
||||
for code in project_codes:
|
||||
list_widget.addItem(code)
|
||||
layout.addWidget(list_widget)
|
||||
|
||||
btn_layout = QtGui.QHBoxLayout()
|
||||
skip_btn = QtGui.QPushButton("Skip")
|
||||
ok_btn = QtGui.QPushButton("Tag Selected")
|
||||
btn_layout.addWidget(skip_btn)
|
||||
btn_layout.addWidget(ok_btn)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
skip_btn.clicked.connect(dialog.reject)
|
||||
ok_btn.clicked.connect(dialog.accept)
|
||||
|
||||
if dialog.exec_() == QtGui.QDialog.Accepted:
|
||||
selected_projects = [
|
||||
item.text() for item in list_widget.selectedItems()
|
||||
]
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"Could not fetch projects: {e}\n")
|
||||
|
||||
try:
|
||||
result = _client.create_item(
|
||||
"kindred-rd",
|
||||
category,
|
||||
description,
|
||||
projects=selected_projects if selected_projects else None,
|
||||
)
|
||||
part_number = result["part_number"]
|
||||
|
||||
if sel:
|
||||
@@ -800,10 +1089,12 @@ class Silo_New:
|
||||
# Create new document
|
||||
_sync.create_document_for_item(result, save=True)
|
||||
|
||||
msg = f"Part number: {part_number}"
|
||||
if selected_projects:
|
||||
msg += f"\nTagged with projects: {', '.join(selected_projects)}"
|
||||
|
||||
FreeCAD.Console.PrintMessage(f"Created: {part_number}\n")
|
||||
QtGui.QMessageBox.information(
|
||||
None, "Item Created", f"Part number: {part_number}"
|
||||
)
|
||||
QtGui.QMessageBox.information(None, "Item Created", msg)
|
||||
|
||||
except Exception as e:
|
||||
QtGui.QMessageBox.critical(None, "Error", str(e))
|
||||
@@ -1143,22 +1434,39 @@ class Silo_Info:
|
||||
item = _client.get_item(part_number)
|
||||
revisions = _client.get_revisions(part_number)
|
||||
|
||||
# Get projects for item
|
||||
try:
|
||||
projects = _client.get_item_projects(part_number)
|
||||
project_codes = [p.get("code", "") for p in projects if p.get("code")]
|
||||
except Exception:
|
||||
project_codes = []
|
||||
|
||||
msg = f"<h3>{part_number}</h3>"
|
||||
msg += f"<p><b>Type:</b> {item.get('item_type', '-')}</p>"
|
||||
msg += f"<p><b>Description:</b> {item.get('description', '-')}</p>"
|
||||
msg += f"<p><b>Projects:</b> {', '.join(project_codes) if project_codes else 'None'}</p>"
|
||||
msg += f"<p><b>Current Revision:</b> {item.get('current_revision', 1)}</p>"
|
||||
msg += f"<p><b>Local Revision:</b> {getattr(obj, 'SiloRevision', '-')}</p>"
|
||||
|
||||
has_file, _ = _client.has_file(part_number)
|
||||
msg += f"<p><b>File in MinIO:</b> {'Yes' if has_file else 'No'}</p>"
|
||||
|
||||
# Show current revision status
|
||||
if revisions:
|
||||
current_status = revisions[0].get("status", "draft")
|
||||
current_labels = revisions[0].get("labels", [])
|
||||
msg += f"<p><b>Current Status:</b> {current_status}</p>"
|
||||
if current_labels:
|
||||
msg += f"<p><b>Labels:</b> {', '.join(current_labels)}</p>"
|
||||
|
||||
msg += "<h4>Revision History</h4><table border='1' cellpadding='4'>"
|
||||
msg += "<tr><th>Rev</th><th>Date</th><th>File</th><th>Comment</th></tr>"
|
||||
msg += "<tr><th>Rev</th><th>Status</th><th>Date</th><th>File</th><th>Comment</th></tr>"
|
||||
for rev in revisions:
|
||||
file_icon = "✓" if rev.get("file_key") else "-"
|
||||
comment = rev.get("comment", "") or "-"
|
||||
date = rev.get("created_at", "")[:10]
|
||||
msg += f"<tr><td>{rev['revision_number']}</td><td>{date}</td><td>{file_icon}</td><td>{comment}</td></tr>"
|
||||
status = rev.get("status", "draft")
|
||||
msg += f"<tr><td>{rev['revision_number']}</td><td>{status}</td><td>{date}</td><td>{file_icon}</td><td>{comment}</td></tr>"
|
||||
msg += "</table>"
|
||||
|
||||
dialog = QtGui.QMessageBox()
|
||||
@@ -1174,6 +1482,309 @@ class Silo_Info:
|
||||
return FreeCAD.ActiveDocument is not None
|
||||
|
||||
|
||||
class Silo_TagProjects:
|
||||
"""Manage project tags for an item."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Tag Projects",
|
||||
"ToolTip": "Add or remove project tags for an item",
|
||||
"Pixmap": _icon("tag"),
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
from PySide import QtGui
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
if not doc:
|
||||
FreeCAD.Console.PrintError("No active document\n")
|
||||
return
|
||||
|
||||
obj = get_tracked_object(doc)
|
||||
if not obj:
|
||||
FreeCAD.Console.PrintError("No tracked object\n")
|
||||
return
|
||||
|
||||
part_number = obj.SiloPartNumber
|
||||
|
||||
try:
|
||||
# Get current projects for item
|
||||
current_projects = _client.get_item_projects(part_number)
|
||||
current_codes = {
|
||||
p.get("code", "") for p in current_projects if p.get("code")
|
||||
}
|
||||
|
||||
# Get all available projects
|
||||
all_projects = _client.get_projects()
|
||||
all_codes = [p.get("code", "") for p in all_projects if p.get("code")]
|
||||
|
||||
if not all_codes:
|
||||
QtGui.QMessageBox.information(
|
||||
None,
|
||||
"Tag Projects",
|
||||
"No projects available. Create projects first.",
|
||||
)
|
||||
return
|
||||
|
||||
# Multi-select dialog
|
||||
dialog = QtGui.QDialog()
|
||||
dialog.setWindowTitle(f"Tag Projects for {part_number}")
|
||||
dialog.setMinimumWidth(350)
|
||||
layout = QtGui.QVBoxLayout(dialog)
|
||||
|
||||
label = QtGui.QLabel("Select projects to associate with this item:")
|
||||
layout.addWidget(label)
|
||||
|
||||
list_widget = QtGui.QListWidget()
|
||||
list_widget.setSelectionMode(QtGui.QAbstractItemView.MultiSelection)
|
||||
for code in all_codes:
|
||||
item = QtGui.QListWidgetItem(code)
|
||||
list_widget.addItem(item)
|
||||
if code in current_codes:
|
||||
item.setSelected(True)
|
||||
layout.addWidget(list_widget)
|
||||
|
||||
btn_layout = QtGui.QHBoxLayout()
|
||||
cancel_btn = QtGui.QPushButton("Cancel")
|
||||
save_btn = QtGui.QPushButton("Save")
|
||||
btn_layout.addWidget(cancel_btn)
|
||||
btn_layout.addWidget(save_btn)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
cancel_btn.clicked.connect(dialog.reject)
|
||||
save_btn.clicked.connect(dialog.accept)
|
||||
|
||||
if dialog.exec_() == QtGui.QDialog.Accepted:
|
||||
selected = [item.text() for item in list_widget.selectedItems()]
|
||||
|
||||
# Add new tags
|
||||
to_add = [c for c in selected if c not in current_codes]
|
||||
if to_add:
|
||||
_client.add_item_projects(part_number, to_add)
|
||||
|
||||
# Note: removing tags would require a separate API call per project
|
||||
# For simplicity, we just add new ones here
|
||||
|
||||
msg = f"Updated project tags for {part_number}"
|
||||
if to_add:
|
||||
msg += f"\nAdded: {', '.join(to_add)}"
|
||||
|
||||
QtGui.QMessageBox.information(None, "Tag Projects", msg)
|
||||
FreeCAD.Console.PrintMessage(f"{msg}\n")
|
||||
|
||||
except Exception as e:
|
||||
QtGui.QMessageBox.warning(None, "Tag Projects", f"Failed: {e}")
|
||||
|
||||
def IsActive(self):
|
||||
return FreeCAD.ActiveDocument is not None
|
||||
|
||||
|
||||
class Silo_Rollback:
|
||||
"""Rollback to a previous revision."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Rollback",
|
||||
"ToolTip": "Rollback to a previous revision",
|
||||
"Pixmap": _icon("rollback"),
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
if not doc:
|
||||
FreeCAD.Console.PrintError("No active document\n")
|
||||
return
|
||||
|
||||
obj = get_tracked_object(doc)
|
||||
if not obj:
|
||||
FreeCAD.Console.PrintError("No tracked object\n")
|
||||
return
|
||||
|
||||
part_number = obj.SiloPartNumber
|
||||
|
||||
try:
|
||||
revisions = _client.get_revisions(part_number)
|
||||
if len(revisions) < 2:
|
||||
QtGui.QMessageBox.information(
|
||||
None, "Rollback", "No previous revisions to rollback to."
|
||||
)
|
||||
return
|
||||
|
||||
# Build revision list for selection (exclude current/latest)
|
||||
current_rev = revisions[0]["revision_number"]
|
||||
prev_revisions = revisions[1:] # All except latest
|
||||
|
||||
# Create selection dialog
|
||||
dialog = QtGui.QDialog()
|
||||
dialog.setWindowTitle(f"Rollback {part_number}")
|
||||
dialog.setMinimumWidth(500)
|
||||
dialog.setMinimumHeight(300)
|
||||
layout = QtGui.QVBoxLayout(dialog)
|
||||
|
||||
label = QtGui.QLabel(
|
||||
f"Select a revision to rollback to (current: Rev {current_rev}):"
|
||||
)
|
||||
layout.addWidget(label)
|
||||
|
||||
# Revision table
|
||||
table = QtGui.QTableWidget()
|
||||
table.setColumnCount(4)
|
||||
table.setHorizontalHeaderLabels(["Rev", "Status", "Date", "Comment"])
|
||||
table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
|
||||
table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
|
||||
table.setRowCount(len(prev_revisions))
|
||||
table.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
for i, rev in enumerate(prev_revisions):
|
||||
table.setItem(i, 0, QtGui.QTableWidgetItem(str(rev["revision_number"])))
|
||||
table.setItem(i, 1, QtGui.QTableWidgetItem(rev.get("status", "draft")))
|
||||
table.setItem(
|
||||
i, 2, QtGui.QTableWidgetItem(rev.get("created_at", "")[:10])
|
||||
)
|
||||
table.setItem(
|
||||
i, 3, QtGui.QTableWidgetItem(rev.get("comment", "") or "")
|
||||
)
|
||||
|
||||
table.resizeColumnsToContents()
|
||||
layout.addWidget(table)
|
||||
|
||||
# Comment field
|
||||
comment_label = QtGui.QLabel("Rollback comment (optional):")
|
||||
layout.addWidget(comment_label)
|
||||
comment_input = QtGui.QLineEdit()
|
||||
comment_input.setPlaceholderText("Reason for rollback...")
|
||||
layout.addWidget(comment_input)
|
||||
|
||||
# Buttons
|
||||
btn_layout = QtGui.QHBoxLayout()
|
||||
cancel_btn = QtGui.QPushButton("Cancel")
|
||||
rollback_btn = QtGui.QPushButton("Rollback")
|
||||
btn_layout.addStretch()
|
||||
btn_layout.addWidget(cancel_btn)
|
||||
btn_layout.addWidget(rollback_btn)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
selected_rev = [None]
|
||||
|
||||
def on_rollback():
|
||||
selected = table.selectedItems()
|
||||
if not selected:
|
||||
QtGui.QMessageBox.warning(
|
||||
dialog, "Rollback", "Please select a revision"
|
||||
)
|
||||
return
|
||||
selected_rev[0] = int(table.item(selected[0].row(), 0).text())
|
||||
dialog.accept()
|
||||
|
||||
cancel_btn.clicked.connect(dialog.reject)
|
||||
rollback_btn.clicked.connect(on_rollback)
|
||||
|
||||
if dialog.exec_() == QtGui.QDialog.Accepted and selected_rev[0]:
|
||||
target_rev = selected_rev[0]
|
||||
comment = comment_input.text().strip()
|
||||
|
||||
# Confirm
|
||||
reply = QtGui.QMessageBox.question(
|
||||
None,
|
||||
"Confirm Rollback",
|
||||
f"Create new revision by rolling back to Rev {target_rev}?\n\n"
|
||||
"This will copy properties and file reference from the selected revision.",
|
||||
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
|
||||
)
|
||||
if reply != QtGui.QMessageBox.Yes:
|
||||
return
|
||||
|
||||
# Perform rollback
|
||||
result = _client.rollback_revision(part_number, target_rev, comment)
|
||||
new_rev = result["revision_number"]
|
||||
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Created revision {new_rev} (rollback from {target_rev})\n"
|
||||
)
|
||||
QtGui.QMessageBox.information(
|
||||
None,
|
||||
"Rollback Complete",
|
||||
f"Created revision {new_rev} from rollback to Rev {target_rev}.\n\n"
|
||||
"Use 'Pull' to download the rolled-back file.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
QtGui.QMessageBox.warning(None, "Rollback", f"Failed: {e}")
|
||||
|
||||
def IsActive(self):
|
||||
return FreeCAD.ActiveDocument is not None
|
||||
|
||||
|
||||
class Silo_SetStatus:
|
||||
"""Set revision status (draft, review, released, obsolete)."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Set Status",
|
||||
"ToolTip": "Set the status of the current revision",
|
||||
"Pixmap": _icon("status"),
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
from PySide import QtGui
|
||||
|
||||
doc = FreeCAD.ActiveDocument
|
||||
if not doc:
|
||||
FreeCAD.Console.PrintError("No active document\n")
|
||||
return
|
||||
|
||||
obj = get_tracked_object(doc)
|
||||
if not obj:
|
||||
FreeCAD.Console.PrintError("No tracked object\n")
|
||||
return
|
||||
|
||||
part_number = obj.SiloPartNumber
|
||||
local_rev = getattr(obj, "SiloRevision", 1)
|
||||
|
||||
try:
|
||||
# Get current revision info
|
||||
revisions = _client.get_revisions(part_number)
|
||||
current_rev = revisions[0] if revisions else None
|
||||
if not current_rev:
|
||||
QtGui.QMessageBox.warning(None, "Set Status", "No revisions found")
|
||||
return
|
||||
|
||||
current_status = current_rev.get("status", "draft")
|
||||
rev_num = current_rev["revision_number"]
|
||||
|
||||
# Status selection
|
||||
statuses = ["draft", "review", "released", "obsolete"]
|
||||
status, ok = QtGui.QInputDialog.getItem(
|
||||
None,
|
||||
"Set Revision Status",
|
||||
f"Set status for Rev {rev_num} (current: {current_status}):",
|
||||
statuses,
|
||||
statuses.index(current_status),
|
||||
False,
|
||||
)
|
||||
|
||||
if not ok or status == current_status:
|
||||
return
|
||||
|
||||
# Update status
|
||||
_client.update_revision(part_number, rev_num, status=status)
|
||||
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Updated Rev {rev_num} status to '{status}'\n"
|
||||
)
|
||||
QtGui.QMessageBox.information(
|
||||
None, "Status Updated", f"Revision {rev_num} status set to '{status}'"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
QtGui.QMessageBox.warning(None, "Set Status", f"Failed: {e}")
|
||||
|
||||
def IsActive(self):
|
||||
return FreeCAD.ActiveDocument is not None
|
||||
|
||||
|
||||
# Register commands
|
||||
FreeCADGui.addCommand("Silo_Open", Silo_Open())
|
||||
FreeCADGui.addCommand("Silo_New", Silo_New())
|
||||
@@ -1182,3 +1793,6 @@ FreeCADGui.addCommand("Silo_Commit", Silo_Commit())
|
||||
FreeCADGui.addCommand("Silo_Pull", Silo_Pull())
|
||||
FreeCADGui.addCommand("Silo_Push", Silo_Push())
|
||||
FreeCADGui.addCommand("Silo_Info", Silo_Info())
|
||||
FreeCADGui.addCommand("Silo_TagProjects", Silo_TagProjects())
|
||||
FreeCADGui.addCommand("Silo_Rollback", Silo_Rollback())
|
||||
FreeCADGui.addCommand("Silo_SetStatus", Silo_SetStatus())
|
||||
|
||||
Reference in New Issue
Block a user