diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py
index 863d6bb9bc..1b92c0d334 100644
--- a/client/ayon_core/tools/workfiles/abstract.py
+++ b/client/ayon_core/tools/workfiles/abstract.py
@@ -1,8 +1,15 @@
+from __future__ import annotations
+
import os
from abc import ABC, abstractmethod
+import typing
+from typing import Optional
from ayon_core.style import get_default_entity_icon_color
+if typing.TYPE_CHECKING:
+ from ayon_core.host import PublishedWorkfileInfo
+
class FolderItem:
"""Item representing folder entity on a server.
@@ -159,6 +166,17 @@ class WorkareaFilepathResult:
self.filepath = filepath
+class PublishedWorkfileWrap:
+ """Wrapper for workfile info that also contains version comment."""
+ def __init__(
+ self,
+ info: Optional[PublishedWorkfileInfo] = None,
+ comment: Optional[str] = None,
+ ) -> None:
+ self.info = info
+ self.comment = comment
+
+
class AbstractWorkfilesCommon(ABC):
@abstractmethod
def is_host_valid(self):
@@ -787,6 +805,25 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
"""
pass
+ @abstractmethod
+ def get_published_workfile_info(
+ self,
+ folder_id: Optional[str],
+ representation_id: Optional[str],
+ ) -> PublishedWorkfileWrap:
+ """Get published workfile info by representation ID.
+
+ Args:
+ folder_id (Optional[str]): Folder id.
+ representation_id (Optional[str]): Representation id.
+
+ Returns:
+ PublishedWorkfileWrap: Published workfile info or None
+ if not found.
+
+ """
+ pass
+
@abstractmethod
def get_workfile_info(self, folder_id, task_id, rootless_path):
"""Workfile info from database.
diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py
index f0e0f0e416..c399a1bf33 100644
--- a/client/ayon_core/tools/workfiles/control.py
+++ b/client/ayon_core/tools/workfiles/control.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
import os
+from typing import Optional
import ayon_api
@@ -18,6 +21,7 @@ from ayon_core.tools.common_models import (
from .abstract import (
AbstractWorkfilesBackend,
AbstractWorkfilesFrontend,
+ PublishedWorkfileWrap,
)
from .models import SelectionModel, WorkfilesModel
@@ -432,6 +436,15 @@ class BaseWorkfileController(
folder_id, task_id
)
+ def get_published_workfile_info(
+ self,
+ folder_id: Optional[str],
+ representation_id: Optional[str],
+ ) -> PublishedWorkfileWrap:
+ return self._workfiles_model.get_published_workfile_info(
+ folder_id, representation_id
+ )
+
def get_workfile_info(self, folder_id, task_id, rootless_path):
return self._workfiles_model.get_workfile_info(
folder_id, task_id, rootless_path
diff --git a/client/ayon_core/tools/workfiles/models/selection.py b/client/ayon_core/tools/workfiles/models/selection.py
index 9a6440b2a1..65caa287d1 100644
--- a/client/ayon_core/tools/workfiles/models/selection.py
+++ b/client/ayon_core/tools/workfiles/models/selection.py
@@ -17,6 +17,8 @@ class SelectionModel(object):
self._task_name = None
self._task_id = None
self._workfile_path = None
+ self._rootless_workfile_path = None
+ self._workfile_entity_id = None
self._representation_id = None
def get_selected_folder_id(self):
@@ -62,39 +64,49 @@ class SelectionModel(object):
def get_selected_workfile_path(self):
return self._workfile_path
+ def get_selected_workfile_data(self):
+ return {
+ "project_name": self._controller.get_current_project_name(),
+ "path": self._workfile_path,
+ "rootless_path": self._rootless_workfile_path,
+ "folder_id": self._folder_id,
+ "task_name": self._task_name,
+ "task_id": self._task_id,
+ "workfile_entity_id": self._workfile_entity_id,
+ }
+
def set_selected_workfile_path(
self, rootless_path, path, workfile_entity_id
):
if path == self._workfile_path:
return
+ self._rootless_workfile_path = rootless_path
self._workfile_path = path
+ self._workfile_entity_id = workfile_entity_id
self._controller.emit_event(
"selection.workarea.changed",
- {
- "project_name": self._controller.get_current_project_name(),
- "path": path,
- "rootless_path": rootless_path,
- "folder_id": self._folder_id,
- "task_name": self._task_name,
- "task_id": self._task_id,
- "workfile_entity_id": workfile_entity_id,
- },
+ self.get_selected_workfile_data(),
self.event_source
)
def get_selected_representation_id(self):
return self._representation_id
+ def get_selected_representation_data(self):
+ return {
+ "project_name": self._controller.get_current_project_name(),
+ "folder_id": self._folder_id,
+ "task_id": self._task_id,
+ "representation_id": self._representation_id,
+ }
+
def set_selected_representation_id(self, representation_id):
if representation_id == self._representation_id:
return
self._representation_id = representation_id
self._controller.emit_event(
"selection.representation.changed",
- {
- "project_name": self._controller.get_current_project_name(),
- "representation_id": representation_id,
- },
+ self.get_selected_representation_data(),
self.event_source
)
diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py
index 5b5591fe43..c15dda2b4f 100644
--- a/client/ayon_core/tools/workfiles/models/workfiles.py
+++ b/client/ayon_core/tools/workfiles/models/workfiles.py
@@ -39,6 +39,7 @@ from ayon_core.pipeline.workfile import (
from ayon_core.pipeline.version_start import get_versioning_start
from ayon_core.tools.workfiles.abstract import (
WorkareaFilepathResult,
+ PublishedWorkfileWrap,
AbstractWorkfilesBackend,
)
@@ -79,6 +80,7 @@ class WorkfilesModel:
# Published workfiles
self._repre_by_id = {}
+ self._version_comment_by_id = {}
self._published_workfile_items_cache = NestedCacheItem(
levels=1, default_factory=list
)
@@ -95,6 +97,7 @@ class WorkfilesModel:
self._workarea_file_items_cache.reset()
self._repre_by_id = {}
+ self._version_comment_by_id = {}
self._published_workfile_items_cache.reset()
self._workfile_entities_by_task_id = {}
@@ -552,13 +555,13 @@ class WorkfilesModel:
)
def get_published_file_items(
- self, folder_id: str, task_id: str
+ self, folder_id: Optional[str], task_id: Optional[str]
) -> list[PublishedWorkfileInfo]:
"""Published workfiles for passed context.
Args:
- folder_id (str): Folder id.
- task_id (str): Task id.
+ folder_id (Optional[str]): Folder id.
+ task_id (Optional[str]): Task id.
Returns:
list[PublishedWorkfileInfo]: List of files for published workfiles.
@@ -586,7 +589,7 @@ class WorkfilesModel:
version_entities = list(ayon_api.get_versions(
project_name,
product_ids=product_ids,
- fields={"id", "author", "taskId"},
+ fields={"id", "author", "taskId", "attrib.comment"},
))
repre_entities = []
@@ -600,6 +603,13 @@ class WorkfilesModel:
repre_entity["id"]: repre_entity
for repre_entity in repre_entities
})
+
+ # Map versions by representation ID for easy lookup
+ self._version_comment_by_id.update({
+ version_entity["id"]: version_entity["attrib"].get("comment")
+ for version_entity in version_entities
+ })
+
project_entity = self._controller.get_project_entity(project_name)
prepared_data = ListPublishedWorkfilesOptionalData(
@@ -626,6 +636,34 @@ class WorkfilesModel:
]
return items
+ def get_published_workfile_info(
+ self,
+ folder_id: Optional[str],
+ representation_id: Optional[str],
+ ) -> PublishedWorkfileWrap:
+ """Get published workfile info by representation ID.
+
+ Args:
+ folder_id (Optional[str]): Folder id.
+ representation_id (Optional[str]): Representation id.
+
+ Returns:
+ PublishedWorkfileWrap: Published workfile info or None
+ if not found.
+
+ """
+ if not representation_id:
+ return PublishedWorkfileWrap()
+
+ # Search through all cached published workfile items
+ for item in self.get_published_file_items(folder_id, None):
+ if item.representation_id == representation_id:
+ comment = self._get_published_workfile_version_comment(
+ representation_id
+ )
+ return PublishedWorkfileWrap(item, comment)
+ return PublishedWorkfileWrap()
+
@property
def _project_name(self) -> str:
return self._controller.get_current_project_name()
@@ -642,6 +680,25 @@ class WorkfilesModel:
self._current_username = get_ayon_username()
return self._current_username
+ def _get_published_workfile_version_comment(
+ self, representation_id: str
+ ) -> Optional[str]:
+ """Get version comment for published workfile.
+
+ Args:
+ representation_id (str): Representation id.
+
+ Returns:
+ Optional[str]: Version comment or None.
+
+ """
+ if not representation_id:
+ return None
+ repre = self._repre_by_id.get(representation_id)
+ if not repre:
+ return None
+ return self._version_comment_by_id.get(repre["versionId"])
+
# --- Host ---
def _open_workfile(self, folder_id: str, task_id: str, filepath: str):
# TODO move to workfiles pipeline
diff --git a/client/ayon_core/tools/workfiles/widgets/side_panel.py b/client/ayon_core/tools/workfiles/widgets/side_panel.py
index b1b91d9721..2929ac780d 100644
--- a/client/ayon_core/tools/workfiles/widgets/side_panel.py
+++ b/client/ayon_core/tools/workfiles/widgets/side_panel.py
@@ -1,6 +1,7 @@
import datetime
+from typing import Optional
-from qtpy import QtWidgets, QtCore
+from qtpy import QtCore, QtWidgets
def file_size_to_string(file_size):
@@ -8,9 +9,9 @@ def file_size_to_string(file_size):
return "N/A"
size = 0
size_ending_mapping = {
- "KB": 1024 ** 1,
- "MB": 1024 ** 2,
- "GB": 1024 ** 3
+ "KB": 1024**1,
+ "MB": 1024**2,
+ "GB": 1024**3,
}
ending = "B"
for _ending, _size in size_ending_mapping.items():
@@ -70,7 +71,12 @@ class SidePanelWidget(QtWidgets.QWidget):
btn_description_save.clicked.connect(self._on_save_click)
controller.register_event_callback(
- "selection.workarea.changed", self._on_selection_change
+ "selection.workarea.changed",
+ self._on_workarea_selection_change
+ )
+ controller.register_event_callback(
+ "selection.representation.changed",
+ self._on_representation_selection_change,
)
self._details_input = details_input
@@ -82,12 +88,13 @@ class SidePanelWidget(QtWidgets.QWidget):
self._task_id = None
self._filepath = None
self._rootless_path = None
+ self._representation_id = None
self._orig_description = ""
self._controller = controller
- self._set_context(None, None, None, None)
+ self._set_context(False, None, None)
- def set_published_mode(self, published_mode):
+ def set_published_mode(self, published_mode: bool) -> None:
"""Change published mode.
Args:
@@ -95,14 +102,37 @@ class SidePanelWidget(QtWidgets.QWidget):
"""
self._description_widget.setVisible(not published_mode)
+ # Clear the context when switching modes to avoid showing stale data
+ if published_mode:
+ self._set_publish_context(
+ self._folder_id,
+ self._task_id,
+ self._representation_id,
+ )
+ else:
+ self._set_workarea_context(
+ self._folder_id,
+ self._task_id,
+ self._rootless_path,
+ self._filepath,
+ )
- def _on_selection_change(self, event):
+ def _on_workarea_selection_change(self, event):
folder_id = event["folder_id"]
task_id = event["task_id"]
filepath = event["path"]
rootless_path = event["rootless_path"]
- self._set_context(folder_id, task_id, rootless_path, filepath)
+ self._set_workarea_context(
+ folder_id, task_id, rootless_path, filepath
+ )
+
+ def _on_representation_selection_change(self, event):
+ folder_id = event["folder_id"]
+ task_id = event["task_id"]
+ representation_id = event["representation_id"]
+
+ self._set_publish_context(folder_id, task_id, representation_id)
def _on_description_change(self):
text = self._description_input.toPlainText()
@@ -118,85 +148,134 @@ class SidePanelWidget(QtWidgets.QWidget):
self._orig_description = description
self._btn_description_save.setEnabled(False)
- def _set_context(self, folder_id, task_id, rootless_path, filepath):
+ def _set_workarea_context(
+ self,
+ folder_id: Optional[str],
+ task_id: Optional[str],
+ rootless_path: Optional[str],
+ filepath: Optional[str],
+ ) -> None:
+ self._rootless_path = rootless_path
+ self._filepath = filepath
+
workfile_info = None
# Check if folder, task and file are selected
if folder_id and task_id and rootless_path:
workfile_info = self._controller.get_workfile_info(
folder_id, task_id, rootless_path
)
- enabled = workfile_info is not None
- self._details_input.setEnabled(enabled)
- self._description_input.setEnabled(enabled)
- self._btn_description_save.setEnabled(enabled)
-
- self._folder_id = folder_id
- self._task_id = task_id
- self._filepath = filepath
- self._rootless_path = rootless_path
-
- # Disable inputs and remove texts if any required arguments are
- # missing
- if not enabled:
+ if workfile_info is None:
self._orig_description = ""
- self._details_input.setPlainText("")
self._description_input.setPlainText("")
+ self._set_context(False, folder_id, task_id)
return
- description = workfile_info.description
- size_value = file_size_to_string(workfile_info.file_size)
+ self._set_context(
+ True,
+ folder_id,
+ task_id,
+ file_created=workfile_info.file_created,
+ file_modified=workfile_info.file_modified,
+ size_value=workfile_info.file_size,
+ created_by=workfile_info.created_by,
+ updated_by=workfile_info.updated_by,
+ )
+
+ description = workfile_info.description
+ self._orig_description = description
+ self._description_input.setPlainText(description)
+
+ def _set_publish_context(
+ self,
+ folder_id: Optional[str],
+ task_id: Optional[str],
+ representation_id: Optional[str],
+ ) -> None:
+ self._representation_id = representation_id
+ published_workfile_wrap = self._controller.get_published_workfile_info(
+ folder_id,
+ representation_id,
+ )
+ info = published_workfile_wrap.info
+ comment = published_workfile_wrap.comment
+ if info is None:
+ self._set_context(False, folder_id, task_id)
+ return
+
+ self._set_context(
+ True,
+ folder_id,
+ task_id,
+ file_created=info.file_created,
+ file_modified=info.file_modified,
+ size_value=info.file_size,
+ created_by=info.author,
+ comment=comment,
+ )
+
+ def _set_context(
+ self,
+ is_valid: bool,
+ folder_id: Optional[str],
+ task_id: Optional[str],
+ *,
+ file_created: Optional[int] = None,
+ file_modified: Optional[int] = None,
+ size_value: Optional[int] = None,
+ created_by: Optional[str] = None,
+ updated_by: Optional[str] = None,
+ comment: Optional[str] = None,
+ ) -> None:
+ self._folder_id = folder_id
+ self._task_id = task_id
+
+ self._details_input.setEnabled(is_valid)
+ self._description_input.setEnabled(is_valid)
+ self._btn_description_save.setEnabled(is_valid)
+ if not is_valid:
+ self._details_input.setPlainText("")
+ return
- # Append html string
datetime_format = "%b %d %Y %H:%M:%S"
- file_created = workfile_info.file_created
- modification_time = workfile_info.file_modified
if file_created:
file_created = datetime.datetime.fromtimestamp(file_created)
- if modification_time:
- modification_time = datetime.datetime.fromtimestamp(
- modification_time)
+ if file_modified:
+ file_modified = datetime.datetime.fromtimestamp(
+ file_modified
+ )
user_items_by_name = self._controller.get_user_items_by_name()
- def convert_username(username):
- user_item = user_items_by_name.get(username)
+ def convert_username(username_v):
+ user_item = user_items_by_name.get(username_v)
if user_item is not None and user_item.full_name:
return user_item.full_name
- return username
+ return username_v
- created_lines = []
- if workfile_info.created_by:
- created_lines.append(
- convert_username(workfile_info.created_by)
- )
- if file_created:
- created_lines.append(file_created.strftime(datetime_format))
+ lines = []
+ if size_value is not None:
+ size_value = file_size_to_string(size_value)
+ lines.append(f"Size:
{size_value}")
- if created_lines:
- created_lines.insert(0, "Created:")
+ # Add version comment for published workfiles
+ if comment:
+ lines.append(f"Comment:
{comment}")
- modified_lines = []
- if workfile_info.updated_by:
- modified_lines.append(
- convert_username(workfile_info.updated_by)
- )
- if modification_time:
- modified_lines.append(
- modification_time.strftime(datetime_format)
- )
- if modified_lines:
- modified_lines.insert(0, "Modified:")
+ if created_by or file_created:
+ lines.append("Created:")
+ if created_by:
+ lines.append(convert_username(created_by))
+ if file_created:
+ lines.append(file_created.strftime(datetime_format))
- lines = (
- "Size:",
- size_value,
- "
".join(created_lines),
- "
".join(modified_lines),
- )
- self._orig_description = description
- self._description_input.setPlainText(description)
+ if updated_by or file_modified:
+ lines.append("Modified:")
+ if updated_by:
+ lines.append(convert_username(updated_by))
+ if file_modified:
+ lines.append(file_modified.strftime(datetime_format))
# Set as empty string
self._details_input.setPlainText("")