From bb983d3f1204a6eb13c730b47cec81d467e7b72e Mon Sep 17 00:00:00 2001 From: vivi295 Date: Mon, 25 May 2026 16:25:44 +0200 Subject: [PATCH 01/51] Load placeholders filter on product base types --- .../ayon_core/pipeline/workfile/workfile_template_builder.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 4d56c32f0ff..bdf91403896 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -1678,12 +1678,10 @@ def _get_representations(self, placeholder): if not folder_ids: return [] - # TODO this should filter by product_base_types - # - that can change only when AYON server 1.14.0 is required products = list(get_products( project_name, folder_ids=folder_ids, - product_types={product_base_type}, + product_base_types={product_base_type}, fields={"id", "name"} )) filtered_product_ids = set() From 7782afc21703d471712863e3b68a3e676ee7b6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 2 Jun 2026 16:28:42 +0200 Subject: [PATCH 02/51] Add plugins to collect and integrate versions to lists --- .../publish/collect_version_to_list.py | 96 +++++++++++++++++++ .../publish/integrate_versions_to_list.py | 43 +++++++++ server/settings/publish_plugins.py | 82 ++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100644 client/ayon_core/plugins/publish/collect_version_to_list.py create mode 100644 client/ayon_core/plugins/publish/integrate_versions_to_list.py diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py new file mode 100644 index 00000000000..76bfba00c60 --- /dev/null +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import platform +from copy import deepcopy +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +import pyblish.api +from ayon_core.lib import StringTemplate, filter_profiles + +if TYPE_CHECKING: + from ayon_core.pipeline import Anatomy + + +@dataclass +class ListConfig: + """Define a list.""" + name: str + parent_folders: list[str] | None = None + is_review_list: bool = False + + +class CollectVersionToList(pyblish.api.InstancePlugin): + """Collect Version to List.""" + + order = pyblish.api.CollectorOrder + 0.499 + label = "Collect Version to List" + + profiles = [] + + def process(self, instance): + profile = self._get_config_from_profile(instance) + if not profile: + self.log.debug(f"No profile found for instance {instance}") + return + + anatomy_data: dict[str, Any] = instance.data["anatomyData"] + anatomy: Anatomy = instance.context.data["anatomy"] + name_template = profile["name_template"] + template_data = deepcopy(anatomy_data) + template_data.update({ + "root": anatomy.roots, + "platform": platform.system().lower(), + }) + + list_name = StringTemplate.format_strict_template( + name_template, template_data) + + version_lists: list[ListConfig] = instance.data.setdefault( + "versionLists", []) + version_lists.append( + ListConfig( + name=list_name, + parent_folders=profile.get("parent_folders", None), + is_review_list=profile.get("is_review_list", False), + ) + ) + + def _get_config_from_profile( + self, + instance: pyblish.api.Instance + ) -> dict | None: + """Returns profile for the given instance.""" + host_name = instance.context.data["hostName"] + product_base_type = instance.data.get("productBaseType") + if not product_base_type: + product_base_type = instance.data["productType"] + product_name = instance.data["productName"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + filtering_criteria = { + "host_names": host_name, + "product_base_types": product_base_type, + "product_names": product_name, + "task_names": task_name, + "task_types": task_type, + } + profile = filter_profiles( + self.profiles, + filtering_criteria, + logger=self.log + ) + + if not profile: + self.log.debug( + "Skipped instance. None of profiles in presets are for" + f' Host name: "{host_name}"' + f' | Product base type: "{product_base_type}"' + f' | Product name: "{product_name}"' + f' | Task name "{task_name}"' + f' | Task type "{task_type}"' + ) + return None + + return profile diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py new file mode 100644 index 00000000000..00f206b3fbc --- /dev/null +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -0,0 +1,43 @@ +import time + +import ayon_api +import pyblish.api + + +class IntegrateVersionToList(pyblish.api.ContextPlugin): + """Integrate published versions to a list + + This plugin integrates published versions to a list in the server. + """ + + label = "Integrate Versions to List" + order = pyblish.api.IntegratorOrder + 0.49 + + def process(self, context): + # formatted timestamp YYYYMMDD-HHMMSS + # e.g. 20230101-120000 + timestamp = time.strftime("%Y%m%d-%H%M%S") + project_name = context.data["projectName"] + list_name_mapping = {} + list_tags: list[str] = [] + for instance in context: + version_entity = instance.data.get("versionEntity") + if not version_entity: + continue + list_name = instance.data.get("addToListName") + if list_name: + list_tags = instance.data.get("addToListTags", []) + list_name = f"{list_name}_{timestamp}" + list_name_mapping[list_name] = [ + *list_name_mapping.get(list_name, []), + version_entity["id"] + ] + + for list_label, version_ids in list_name_mapping.items(): + ayon_api.create_entity_list( + project_name=project_name, + entity_type="version", + label=list_label, + items=[{"entityId": version_id} for version_id in version_ids], + tags=list_tags, + ) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index f32e25e5c1e..700b4bf5395 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -201,6 +201,72 @@ def validate_unique_outputs(cls, value): return value +class CollectVersionToListProfileModel(BaseSettingsModel): + """.""" + _layout = "expanded" + host_names: list[str] = SettingsField( + default_factory=list, + title="Host names", + description="The host names to match this profile to.", + section="Filter", + ) + task_types: list[str] = SettingsField( + default_factory=list, + title="Task Types", + enum_resolver=task_types_enum, + description=( + "The current create context task type to filter against. This" + " allows to filter the profile to only be valid if currently " + " creating from within that task type." + ), + ) + task_names: list[str] = SettingsField( + default_factory=list, + title="Task names", + description="The task names to match this profile to.", + ) + product_base_types: list[str] = SettingsField( + default_factory=list, + title="Product base types", + description=( + "The product base types to match this profile to. When matched," + " the settings below would apply to the instance as default" + " attributes." + ) + ) + product_names: list[str] = SettingsField( + default_factory=list, + title="Product names", + description="The product names to match this profile to.", + ) + name_template: str = SettingsField( + "", + title="Name template", + description="Anatomy formattable template for the name.", + section="List configuration", + ) + parent_folders: list[str] = SettingsField( + default_factory=list, + title="Parent folders", + description="Folder hierarchy formed from top to bottom.", + ) + is_review_list: bool = SettingsField( + False, + title="Is review list", + description="", + ) + + +class CollectVersionToListModel(BaseSettingsModel): + enabled: bool = SettingsField(True, title="Enabled") + profiles: list[CollectVersionToListProfileModel] = SettingsField( + default_factory=list, + title="Profiles", + description=( + "" + ) + ) + class ResolutionOptionsModel(BaseSettingsModel): _layout = "compact" width: int = SettingsField( @@ -1226,6 +1292,10 @@ class PublishPuginsModel(BaseSettingsModel): title="Collect USD Layer Contributions", ) ) + CollectVersionToList: CollectVersionToListModel = SettingsField( + default_factory=CollectVersionToListModel, + title="Collect Version to List", + ) CollectExplicitResolution: CollectExplicitResolutionModel = SettingsField( default_factory=CollectExplicitResolutionModel, title="Collect Explicit Resolution" @@ -1429,6 +1499,18 @@ class PublishPuginsModel(BaseSettingsModel): }, ] }, + "CollectVersionToList": { + "enabled": True, + "profiles": [ + { + "host_names": [], + "task_types": [], + "task_names": [], + "product_base_types": [], + "product_names": [], + } + ] + }, "CollectExplicitResolution": { "enabled": True, "product_base_types": [ From 45718c70c50bbd8afd182ff71eb0dc86f285b050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 3 Jun 2026 11:18:10 +0200 Subject: [PATCH 03/51] Refactor entity list integration with folder support Add support for organizing version lists under parent folders and checking for existing lists before creation. Extract helper methods for managing list folders, creating lists, and updating list items. Replace timestamp-based naming with direct list configuration. --- .../publish/integrate_versions_to_list.py | 215 ++++++++++++++++-- 1 file changed, 194 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 00f206b3fbc..a1e76ffc652 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -1,8 +1,15 @@ -import time +from __future__ import annotations + +from typing import TYPE_CHECKING import ayon_api +from ayon_api.utils import create_entity_id import pyblish.api +if TYPE_CHECKING: + from ayon_core.plugins.publish.collect_version_to_list import ListConfig + + class IntegrateVersionToList(pyblish.api.ContextPlugin): """Integrate published versions to a list @@ -14,30 +21,196 @@ class IntegrateVersionToList(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder + 0.49 def process(self, context): - # formatted timestamp YYYYMMDD-HHMMSS - # e.g. 20230101-120000 - timestamp = time.strftime("%Y%m%d-%H%M%S") project_name = context.data["projectName"] list_name_mapping = {} - list_tags: list[str] = [] + # TODO: + # - check if list with the name already exists + # - if exists: + # - append version to the list + # - parent folders are ignored + # - if not exists: + # - create a new list and create + # - create parent folders if specified for instance in context: + version_lists: list[ListConfig] = instance.data.get( + "versionLists", []) version_entity = instance.data.get("versionEntity") - if not version_entity: + if not version_entity or not version_lists: + continue + for list_config in version_lists: + list_data = list_name_mapping.setdefault( + list_config.name, {}) + list_data.update({ + "parent_folders": list_config.parent_folders, + "is_review_list": list_config.is_review_list, + }) + version_ids = list_data.setdefault( + "version_ids", []) + version_ids.append(version_entity["id"]) + + for list_label, list_data in list_name_mapping.items(): + version_ids = list_data["version_ids"] + is_review_list = list_data["is_review_list"] + parent_folders = list_data["parent_folders"] + # check first if list exists + existing_list = self._existing_list( + project_name=project_name, + list_label=list_label, + list_type="review-session" if is_review_list else None, + ) + if existing_list: + self._update_entity_list( + project_name=project_name, + list_entity=existing_list, + version_ids=version_ids, + ) + else: + entity_list_folder_id = None + # make sure parent folder exists + if parent_folders: + entity_list_folder_id = self._get_or_create_parent_folder( + project_name=project_name, + parent_folders=parent_folders, + ) + # then create list under a parent folder + self._create_entity_list( + list_type="review-session" if is_review_list else None, + project_name=project_name, + entity_type="version", + label=list_label, + entity_list_folder_id=entity_list_folder_id, + items=[ + {"entityId": version_id} + for version_id in version_ids + ], + ) + + def _get_or_create_parent_folder( + self, + project_name: str, + parent_folders: list[str], + ) -> str | None: + if not parent_folders: + return None + existing_list_folders = self._get_list_folders_helper( + project_name=project_name + ) + parent_folder_id = None + for folder_name in parent_folders: + # make sure to get existing folder id if it exists + # and use it instead of creating a new one + existing_list_folder_id = None + for list_folder in existing_list_folders: + if list_folder["label"] == folder_name: + existing_list_folder_id = list_folder["id"] + break + if existing_list_folder_id: + parent_folder_id = existing_list_folder_id continue - list_name = instance.data.get("addToListName") - if list_name: - list_tags = instance.data.get("addToListTags", []) - list_name = f"{list_name}_{timestamp}" - list_name_mapping[list_name] = [ - *list_name_mapping.get(list_name, []), - version_entity["id"] - ] - - for list_label, version_ids in list_name_mapping.items(): - ayon_api.create_entity_list( + # if folder does not exist, create it + # under the existing parent folder + parent_folder_id = self._create_list_folder_helper( + project_name=project_name, + folder_name=folder_name, + parent_folder_id=parent_folder_id, + ) + return parent_folder_id + + def _create_list_folder_helper( + self, + project_name: str, + folder_name: str, + parent_folder_id: str | None = None, + ) -> str: + kwargs = { + "id": create_entity_id(), + "label": folder_name, + } + if parent_folder_id: + kwargs["parentId"] = parent_folder_id + response = ayon_api.post( + f"projects/{project_name}/entityListFolders", + **kwargs, + ) + response.raise_for_status() + self.log.debug(f"List folder created: {response.json()}") + return response.json()["id"] + + def _get_list_folders_helper( + self, + project_name: str, + ) -> list[dict]: + response = ayon_api.get( + f"projects/{project_name}/entityListFolders", + ) + response.raise_for_status() + self.log.debug(f"List folders: {response.json()}") + return response.json()["folders"] + + def _update_entity_list( + self, + project_name: str, + list_entity: dict, + version_ids: list[str] | None = None, + ): + if version_ids is None: + return + + for entity_id in version_ids: + item_id = ayon_api.create_entity_list_item( project_name=project_name, - entity_type="version", - label=list_label, - items=[{"entityId": version_id} for version_id in version_ids], - tags=list_tags, + list_id=list_entity["id"], + entity_id=entity_id, + ) + self.log.info( + f"Created entity list item {item_id} for version {entity_id}" + f" in list {list_entity['label']}" ) + + def _create_entity_list( + self, + project_name: str, + entity_type: str, + label: str, + list_type: str | None = None, + entity_list_folder_id: str | None = None, + items: list[dict] | None = None, + ) -> dict: + kwargs = { + "id": create_entity_id(), + "entityType": entity_type, + "label": label, + } + for key, value in ( + ("entityListType", list_type), + ("entityListFolderId", entity_list_folder_id), + ): + if value is not None: + kwargs[key] = value + + self.log.debug(f"Creating entity list: {kwargs}") + response = ayon_api.post( + f"projects/{project_name}/lists", + **kwargs + ) + response.raise_for_status() + self.log.debug(f"Entity list created: {response.json()}") + return response.json() + + @staticmethod + def _existing_list( + project_name: str, + list_label: str, + list_type: str | None = None, + ) -> dict | None: + all_lists = ayon_api.get_entity_lists( + project_name=project_name, + ) + # iterate via all lists and check if the label matches + for list_data in all_lists: + if list_type and list_data["entityListType"] != list_type: + continue + if list_data["label"] != list_label: + continue + return list_data + return None From 6576c3397fe82d91daa844acd7b8e5f5c05e582d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 3 Jun 2026 11:18:18 +0200 Subject: [PATCH 04/51] Remove unnecessary whitespace and empty profile --- .../plugins/publish/collect_version_to_list.py | 1 - server/settings/publish_plugins.py | 10 +--------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index 76bfba00c60..d201af4fc53 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -45,7 +45,6 @@ def process(self, instance): list_name = StringTemplate.format_strict_template( name_template, template_data) - version_lists: list[ListConfig] = instance.data.setdefault( "versionLists", []) version_lists.append( diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 700b4bf5395..e3a11aa26fe 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1501,15 +1501,7 @@ class PublishPuginsModel(BaseSettingsModel): }, "CollectVersionToList": { "enabled": True, - "profiles": [ - { - "host_names": [], - "task_types": [], - "task_names": [], - "product_base_types": [], - "product_names": [], - } - ] + "profiles": [] }, "CollectExplicitResolution": { "enabled": True, From 66f4556aaf5306a01adcd4161b562e5a86305bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 3 Jun 2026 11:30:06 +0200 Subject: [PATCH 05/51] Add module docstring and clean up imports Remove outdated TODO comment and reorganize imports alphabetically. Update class docstring to end with period. --- .../publish/integrate_versions_to_list.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index a1e76ffc652..0101b79d403 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -1,18 +1,24 @@ +"""Pyblish context plugin that adds published versions to AYON entity lists. + +For each instance with ``versionLists`` data, creates or updates the named +lists (optionally typed as ``review-session``) and organises them under the +requested folder hierarchy on the server. +""" + from __future__ import annotations from typing import TYPE_CHECKING import ayon_api -from ayon_api.utils import create_entity_id import pyblish.api +from ayon_api.utils import create_entity_id if TYPE_CHECKING: from ayon_core.plugins.publish.collect_version_to_list import ListConfig - class IntegrateVersionToList(pyblish.api.ContextPlugin): - """Integrate published versions to a list + """Integrate published versions to a list. This plugin integrates published versions to a list in the server. """ @@ -23,14 +29,6 @@ class IntegrateVersionToList(pyblish.api.ContextPlugin): def process(self, context): project_name = context.data["projectName"] list_name_mapping = {} - # TODO: - # - check if list with the name already exists - # - if exists: - # - append version to the list - # - parent folders are ignored - # - if not exists: - # - create a new list and create - # - create parent folders if specified for instance in context: version_lists: list[ListConfig] = instance.data.get( "versionLists", []) From 6eedf88657e47c226146cc508b49a6d595e016ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 3 Jun 2026 14:28:50 +0200 Subject: [PATCH 06/51] Support version list templates in publish process Add support for `versionListsTemplates` instance data alongside profile-based configuration. Allow processing multiple list templates with custom template data. Replace `response.json()` calls with `response.data` for consistency across both plugins. --- .../publish/collect_version_to_list.py | 34 +++++++++++++------ .../publish/integrate_versions_to_list.py | 12 +++---- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index d201af4fc53..ba6025af897 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -29,31 +29,43 @@ class CollectVersionToList(pyblish.api.InstancePlugin): profiles = [] def process(self, instance): + version_lists_templates = instance.data.get( + "versionListsTemplates", []) profile = self._get_config_from_profile(instance) - if not profile: + if not profile and not version_lists_templates: self.log.debug(f"No profile found for instance {instance}") return + processing_items = [] + if version_lists_templates: + processing_items += version_lists_templates + if profile: + processing_items.append(profile) + anatomy_data: dict[str, Any] = instance.data["anatomyData"] anatomy: Anatomy = instance.context.data["anatomy"] - name_template = profile["name_template"] template_data = deepcopy(anatomy_data) template_data.update({ "root": anatomy.roots, "platform": platform.system().lower(), }) - - list_name = StringTemplate.format_strict_template( - name_template, template_data) version_lists: list[ListConfig] = instance.data.setdefault( "versionLists", []) - version_lists.append( - ListConfig( - name=list_name, - parent_folders=profile.get("parent_folders", None), - is_review_list=profile.get("is_review_list", False), + for item in processing_items: + name_template = item["name_template"] + data = item.get("data", {}) + if data: + template_data.update(data) + list_name = StringTemplate.format_strict_template( + name_template, template_data) + version_lists.append( + ListConfig( + name=list_name, + parent_folders=item.get("parent_folders", None), + is_review_list=item.get("is_review_list", False), + ) ) - ) + self.log.debug(f"Collected version lists: {version_lists}") def _get_config_from_profile( self, diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 0101b79d403..f04bb6c3590 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -131,8 +131,8 @@ def _create_list_folder_helper( **kwargs, ) response.raise_for_status() - self.log.debug(f"List folder created: {response.json()}") - return response.json()["id"] + self.log.debug(f"List folder created: {response.data}") + return response.data["id"] def _get_list_folders_helper( self, @@ -142,8 +142,8 @@ def _get_list_folders_helper( f"projects/{project_name}/entityListFolders", ) response.raise_for_status() - self.log.debug(f"List folders: {response.json()}") - return response.json()["folders"] + self.log.debug(f"List folders: {response.data}") + return response.data["folders"] def _update_entity_list( self, @@ -192,8 +192,8 @@ def _create_entity_list( **kwargs ) response.raise_for_status() - self.log.debug(f"Entity list created: {response.json()}") - return response.json() + self.log.debug(f"Entity list created: {response.data}") + return response.data @staticmethod def _existing_list( From d087b3475469eb38b2923b55b3a42960e748f539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 3 Jun 2026 14:53:55 +0200 Subject: [PATCH 07/51] Add early exit for empty version lists Skip processing lists with no version IDs. Add TODO comments for future ayon_api compatibility. Extract create_entity_list_item as a module-level function and adjust parameter order in _create_entity_list_helper. --- .../publish/integrate_versions_to_list.py | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index f04bb6c3590..79076e3065b 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -48,6 +48,10 @@ def process(self, context): for list_label, list_data in list_name_mapping.items(): version_ids = list_data["version_ids"] + if not version_ids: + self.log.debug(f"No version ids for list: {list_label}") + continue + is_review_list = list_data["is_review_list"] parent_folders = list_data["parent_folders"] # check first if list exists @@ -114,6 +118,7 @@ def _get_or_create_parent_folder( ) return parent_folder_id + # TODO: ayon_api future compatibility, remove once ayon_api supports it def _create_list_folder_helper( self, project_name: str, @@ -134,6 +139,7 @@ def _create_list_folder_helper( self.log.debug(f"List folder created: {response.data}") return response.data["id"] + # TODO: ayon_api future compatibility, remove once ayon_api supports it def _get_list_folders_helper( self, project_name: str, @@ -155,7 +161,7 @@ def _update_entity_list( return for entity_id in version_ids: - item_id = ayon_api.create_entity_list_item( + item_id = create_entity_list_item( project_name=project_name, list_id=list_entity["id"], entity_id=entity_id, @@ -170,14 +176,15 @@ def _create_entity_list( project_name: str, entity_type: str, label: str, + items: list[dict], list_type: str | None = None, entity_list_folder_id: str | None = None, - items: list[dict] | None = None, ) -> dict: kwargs = { "id": create_entity_id(), "entityType": entity_type, "label": label, + "items": items, } for key, value in ( ("entityListType", list_type), @@ -212,3 +219,34 @@ def _existing_list( continue return list_data return None + + +# TODO: ayon_api future compatibility, remove once ayon_api supports it +def create_entity_list_item( + project_name: str, + list_id: str, + entity_id: str, +) -> str: + """Create entity list item. + + Args: + project_name (str): Project name where entity list lives. + list_id (str): Entity list id where item will be added. + entity_id (str): Id of entity added to the list. + + Returns: + str: Item id. + + """ + item_id = create_entity_id() + kwargs = { + "id": item_id, + "entityId": entity_id, + } + + response = ayon_api.post( + f"projects/{project_name}/lists/{list_id}/items", + **kwargs + ) + response.raise_for_status() + return item_id From 038932bae0ef228214d3bfcdb4585569828dde0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 3 Jun 2026 14:59:01 +0200 Subject: [PATCH 08/51] added blank line --- server/settings/publish_plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index e3a11aa26fe..575039123c3 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -267,6 +267,7 @@ class CollectVersionToListModel(BaseSettingsModel): ) ) + class ResolutionOptionsModel(BaseSettingsModel): _layout = "compact" width: int = SettingsField( From a626f2ab0bc385e286a50240f61bca9842361735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 3 Jun 2026 16:08:46 +0200 Subject: [PATCH 09/51] Rename method to clarify it gets profile for instance --- client/ayon_core/plugins/publish/collect_version_to_list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index ba6025af897..8c498202148 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -31,7 +31,7 @@ class CollectVersionToList(pyblish.api.InstancePlugin): def process(self, instance): version_lists_templates = instance.data.get( "versionListsTemplates", []) - profile = self._get_config_from_profile(instance) + profile = self._get_profile_for_instance(instance) if not profile and not version_lists_templates: self.log.debug(f"No profile found for instance {instance}") return @@ -67,7 +67,7 @@ def process(self, instance): ) self.log.debug(f"Collected version lists: {version_lists}") - def _get_config_from_profile( + def _get_profile_for_instance( self, instance: pyblish.api.Instance ) -> dict | None: From e555f81ecb9f2807ea9236ab586a7498514d79ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 3 Jun 2026 16:36:15 +0200 Subject: [PATCH 10/51] Update client/ayon_core/plugins/publish/collect_version_to_list.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/collect_version_to_list.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index 8c498202148..208143065f3 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -42,9 +42,8 @@ def process(self, instance): if profile: processing_items.append(profile) - anatomy_data: dict[str, Any] = instance.data["anatomyData"] anatomy: Anatomy = instance.context.data["anatomy"] - template_data = deepcopy(anatomy_data) + template_data = deepcopy(instance.data["anatomyData"]) template_data.update({ "root": anatomy.roots, "platform": platform.system().lower(), From f73093007f5a14df73f66840a8cdb76ff1e9fa41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 3 Jun 2026 16:56:24 +0200 Subject: [PATCH 11/51] Extract ListConfig to shared structures module --- client/ayon_core/pipeline/structures.py | 13 +++++++++++++ .../plugins/publish/collect_version_to_list.py | 10 +--------- .../publish/integrate_versions_to_list.py | 2 +- server/settings/publish_plugins.py | 16 ++++++++++++---- 4 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 client/ayon_core/pipeline/structures.py diff --git a/client/ayon_core/pipeline/structures.py b/client/ayon_core/pipeline/structures.py new file mode 100644 index 00000000000..d4669ae312a --- /dev/null +++ b/client/ayon_core/pipeline/structures.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +ListType = Literal["generic", "review-session"] + +@dataclass +class ListConfig: + """Define a list.""" + name: str + parent_folders: list[str] | None = None + list_type: ListType = "generic" diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index 8c498202148..1503fdf2975 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -2,24 +2,16 @@ import platform from copy import deepcopy -from dataclasses import dataclass from typing import TYPE_CHECKING, Any import pyblish.api from ayon_core.lib import StringTemplate, filter_profiles +from ayon_core.pipeline.structures import ListConfig if TYPE_CHECKING: from ayon_core.pipeline import Anatomy -@dataclass -class ListConfig: - """Define a list.""" - name: str - parent_folders: list[str] | None = None - is_review_list: bool = False - - class CollectVersionToList(pyblish.api.InstancePlugin): """Collect Version to List.""" diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 79076e3065b..921c19d8a09 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -14,7 +14,7 @@ from ayon_api.utils import create_entity_id if TYPE_CHECKING: - from ayon_core.plugins.publish.collect_version_to_list import ListConfig + from ayon_core.pipeline.structures import ListConfig class IntegrateVersionToList(pyblish.api.ContextPlugin): diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 575039123c3..18651e6a058 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -201,6 +201,12 @@ def validate_unique_outputs(cls, value): return value +def list_type_enum(): + return [ + {"label": "Generic", "value": "generic"}, + {"label": "Review Session", "value": "review-session"}, + ] + class CollectVersionToListProfileModel(BaseSettingsModel): """.""" _layout = "expanded" @@ -250,10 +256,12 @@ class CollectVersionToListProfileModel(BaseSettingsModel): title="Parent folders", description="Folder hierarchy formed from top to bottom.", ) - is_review_list: bool = SettingsField( - False, - title="Is review list", - description="", + list_type: str = SettingsField( + "generic", + title="List type", + description="Define what type of list this profile represents.", + enum_resolver=list_type_enum, + ) From 589d4b72aa67fdc3aed9354e7c21ade3dec59c17 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Jun 2026 17:23:13 +0200 Subject: [PATCH 12/51] Apply filter based on server version because it'll behave differently around the 1.14.0 cutoff --- .../pipeline/workfile/workfile_template_builder.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index bdf91403896..f020bef817b 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -1678,10 +1678,17 @@ def _get_representations(self, placeholder): if not folder_ids: return [] + filter_key = "product_base_types" + if ayon_api.get_server_version_tuple() < (1, 14, 0): + filter_key = "product_types" + filter_kwargs = { + filter_key: {product_base_type}, + } + products = list(get_products( project_name, folder_ids=folder_ids, - product_base_types={product_base_type}, + **filter_kwargs, fields={"id", "name"} )) filtered_product_ids = set() From 8fe7c6cff6496c70757a71996cf440014b84b949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 3 Jun 2026 18:11:35 +0200 Subject: [PATCH 13/51] Simplify version list collection and integrate template formatting The collection plugin now only processes profiles and skips template formatting, delegating that to the integration plugin. The integration plugin now handles all template formatting for list names and parent folders before creating or updating lists. --- .../publish/collect_version_to_list.py | 51 +++++-------------- .../publish/integrate_versions_to_list.py | 51 ++++++++++++++----- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index fd5d53528e6..fccbb5ade18 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -1,16 +1,9 @@ from __future__ import annotations -import platform -from copy import deepcopy -from typing import TYPE_CHECKING, Any - import pyblish.api -from ayon_core.lib import StringTemplate, filter_profiles +from ayon_core.lib import filter_profiles from ayon_core.pipeline.structures import ListConfig -if TYPE_CHECKING: - from ayon_core.pipeline import Anatomy - class CollectVersionToList(pyblish.api.InstancePlugin): """Collect Version to List.""" @@ -21,41 +14,25 @@ class CollectVersionToList(pyblish.api.InstancePlugin): profiles = [] def process(self, instance): - version_lists_templates = instance.data.get( - "versionListsTemplates", []) profile = self._get_profile_for_instance(instance) - if not profile and not version_lists_templates: + if not profile: self.log.debug(f"No profile found for instance {instance}") return - - processing_items = [] - if version_lists_templates: - processing_items += version_lists_templates - if profile: - processing_items.append(profile) - - anatomy: Anatomy = instance.context.data["anatomy"] - template_data = deepcopy(instance.data["anatomyData"]) - template_data.update({ - "root": anatomy.roots, - "platform": platform.system().lower(), - }) version_lists: list[ListConfig] = instance.data.setdefault( "versionLists", []) - for item in processing_items: - name_template = item["name_template"] - data = item.get("data", {}) - if data: - template_data.update(data) - list_name = StringTemplate.format_strict_template( - name_template, template_data) - version_lists.append( - ListConfig( - name=list_name, - parent_folders=item.get("parent_folders", None), - is_review_list=item.get("is_review_list", False), - ) + + if version_lists: + self.log.debug(f"Version lists already collected: {version_lists}") + return + + name_template = profile["name_template"] + version_lists.append( + ListConfig( + name=name_template, + parent_folders=profile.get("parent_folders", None), + list_type=profile["list_type"], ) + ) self.log.debug(f"Collected version lists: {version_lists}") def _get_profile_for_instance( diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 921c19d8a09..b951a39cc5c 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -7,13 +7,17 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import platform +from copy import deepcopy +from typing import TYPE_CHECKING, Any, Generator import ayon_api import pyblish.api from ayon_api.utils import create_entity_id +from ayon_core.lib import StringTemplate if TYPE_CHECKING: + from ayon_core.pipeline import Anatomy from ayon_core.pipeline.structures import ListConfig @@ -36,29 +40,50 @@ def process(self, context): if not version_entity or not version_lists: continue for list_config in version_lists: + anatomy: Anatomy = instance.context.data["anatomy"] + template_keys = deepcopy(instance.data["anatomyData"]) + template_keys.update({ + "root": anatomy.roots, + "platform": platform.system().lower(), + }) + list_name = StringTemplate.format_strict_template( + list_config.name, template_keys) + list_data = list_name_mapping.setdefault( - list_config.name, {}) + list_name, {}) + parent_folders: list[str] | None + if parent_folders := list_config.parent_folders: + parent_folders = [ + StringTemplate.format_template( + folder, + template_keys + ) + for folder in parent_folders + ] list_data.update({ - "parent_folders": list_config.parent_folders, - "is_review_list": list_config.is_review_list, + "parent_folders": parent_folders, + "list_type": list_config.list_type, }) version_ids = list_data.setdefault( "version_ids", []) version_ids.append(version_entity["id"]) + # Get all list entities for the project from server + all_list_entities = ayon_api.get_entity_lists( + project_name=project_name, + ) for list_label, list_data in list_name_mapping.items(): version_ids = list_data["version_ids"] if not version_ids: self.log.debug(f"No version ids for list: {list_label}") continue - is_review_list = list_data["is_review_list"] parent_folders = list_data["parent_folders"] # check first if list exists existing_list = self._existing_list( - project_name=project_name, + all_list_entities=all_list_entities, list_label=list_label, - list_type="review-session" if is_review_list else None, + list_type=list_data["list_type"], ) if existing_list: self._update_entity_list( @@ -204,16 +229,14 @@ def _create_entity_list( @staticmethod def _existing_list( - project_name: str, + all_list_entities: Generator[dict[str, Any], None, None], list_label: str, - list_type: str | None = None, + list_type: str, ) -> dict | None: - all_lists = ayon_api.get_entity_lists( - project_name=project_name, - ) + # iterate via all lists and check if the label matches - for list_data in all_lists: - if list_type and list_data["entityListType"] != list_type: + for list_data in all_list_entities: + if list_data["entityListType"] != list_type: continue if list_data["label"] != list_label: continue From 9922903f1e311636e410834a99b77694839cfa74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 3 Jun 2026 18:12:19 +0200 Subject: [PATCH 14/51] Use list_type from data instead of hardcoding --- client/ayon_core/plugins/publish/integrate_versions_to_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index b951a39cc5c..f8e59b19d73 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -101,7 +101,7 @@ def process(self, context): ) # then create list under a parent folder self._create_entity_list( - list_type="review-session" if is_review_list else None, + list_type=list_data["list_type"], project_name=project_name, entity_type="version", label=list_label, From 10821af24769d3a3be8797b13bed335de2cb8396 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Jun 2026 23:20:43 +0200 Subject: [PATCH 15/51] Restructure code --- .../publish/integrate_versions_to_list.py | 285 +++++++++--------- 1 file changed, 149 insertions(+), 136 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index f8e59b19d73..4f1bbd3fe04 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -1,7 +1,7 @@ """Pyblish context plugin that adds published versions to AYON entity lists. For each instance with ``versionLists`` data, creates or updates the named -lists (optionally typed as ``review-session``) and organises them under the +lists (optionally typed as ``review-session``) and organizes them under the requested folder hierarchy on the server. """ @@ -9,6 +9,7 @@ import platform from copy import deepcopy +from collections import defaultdict from typing import TYPE_CHECKING, Any, Generator import ayon_api @@ -21,18 +22,83 @@ from ayon_core.pipeline.structures import ListConfig +# TODO: ayon_api future compatibility, remove once ayon_api supports it +def create_entity_list_item( + project_name: str, + list_id: str, + entity_id: str, +) -> str: + """Create entity list item. + + Args: + project_name (str): Project name where entity list lives. + list_id (str): Entity list id where item will be added. + entity_id (str): Id of entity added to the list. + + Returns: + str: Item id. + + """ + item_id = create_entity_id() + kwargs = { + "id": item_id, + "entityId": entity_id, + } + + response = ayon_api.post( + f"projects/{project_name}/lists/{list_id}/items", + **kwargs + ) + response.raise_for_status() + return item_id + + +# TODO: ayon_api future compatibility, remove once ayon_api supports it +def create_entity_list_folder( + project_name: str, + folder_name: str, + parent_folder_id: str | None = None, +) -> str: + kwargs = { + "id": create_entity_id(), + "label": folder_name, + } + if parent_folder_id: + kwargs["parentId"] = parent_folder_id + response = ayon_api.post( + f"projects/{project_name}/entityListFolders", + **kwargs, + ) + response.raise_for_status() + return response.data["id"] + + +# TODO: ayon_api future compatibility, remove once ayon_api supports it +def get_entity_list_folders(project_name: str) -> list[dict]: + response = ayon_api.get( + f"projects/{project_name}/entityListFolders", + ) + response.raise_for_status() + return response.data["folders"] + + class IntegrateVersionToList(pyblish.api.ContextPlugin): """Integrate published versions to a list. This plugin integrates published versions to a list in the server. + + Note that list names are unique; as such, even if multiple list configs + are set to go to different folders but with the same list name - then all + versions will be integrated to the same list regardless of what list folder + it is in. """ label = "Integrate Versions to List" order = pyblish.api.IntegratorOrder + 0.49 def process(self, context): - project_name = context.data["projectName"] - list_name_mapping = {} + list_config_by_list_name: dict[str, ListConfig] = {} + version_ids_by_list_name: dict[str, list[str]] = defaultdict(list) for instance in context: version_lists: list[ListConfig] = instance.data.get( "versionLists", []) @@ -40,71 +106,93 @@ def process(self, context): if not version_entity or not version_lists: continue for list_config in version_lists: + # Construct the list config with the formatted name and parent + # folder names anatomy: Anatomy = instance.context.data["anatomy"] template_keys = deepcopy(instance.data["anatomyData"]) template_keys.update({ "root": anatomy.roots, "platform": platform.system().lower(), }) - list_name = StringTemplate.format_strict_template( - list_config.name, template_keys) - - list_data = list_name_mapping.setdefault( - list_name, {}) + list_name: str = str( + StringTemplate.format_strict_template( + list_config.name, template_keys + ) + ) parent_folders: list[str] | None if parent_folders := list_config.parent_folders: parent_folders = [ - StringTemplate.format_template( + str(StringTemplate.format_template( folder, template_keys - ) + )) for folder in parent_folders ] - list_data.update({ - "parent_folders": parent_folders, - "list_type": list_config.list_type, - }) - version_ids = list_data.setdefault( - "version_ids", []) - version_ids.append(version_entity["id"]) + + # Define the list config, and add the version id to the list + list_config_by_list_name[list_name] = ListConfig( + name=list_name, + parent_folders=parent_folders, + list_type=list_config.list_type, + ) + version_ids_by_list_name[list_name].append( + version_entity["id"] + ) # Get all list entities for the project from server - all_list_entities = ayon_api.get_entity_lists( - project_name=project_name, - ) - for list_label, list_data in list_name_mapping.items(): - version_ids = list_data["version_ids"] + project_name: str = context.data["projectName"] + existing_list_entities_by_label: dict[str, dict] = { + entity_list["label"]: entity_list + for entity_list in ayon_api.get_entity_lists( + project_name=project_name + ) + } + for list_name, list_config in list_config_by_list_name.items(): + version_ids = version_ids_by_list_name[list_name] if not version_ids: - self.log.debug(f"No version ids for list: {list_label}") + self.log.debug(f"No version ids for list: {list_name}") continue - parent_folders = list_data["parent_folders"] - # check first if list exists - existing_list = self._existing_list( - all_list_entities=all_list_entities, - list_label=list_label, - list_type=list_data["list_type"], - ) + # If list exists, append to it but ensure it is of correct type + existing_list = existing_list_entities_by_label.get(list_name) if existing_list: - self._update_entity_list( + if existing_list["entityListType"] != list_config.list_type: + entity_list_type = existing_list["entityListType"] + self.log.error( + "Can't add versions to list because another entity" + f" list type '{entity_list_type}' with that label" + f" already exists: {list_config.name}" + ) + continue + + if existing_list["entityType"] != "version": + entity_type = existing_list["entityType"] + self.log.error( + f"Can't add versions to list because a '{entity_type}'" + " list type with that label already exists: " + f"{list_config.name}" + ) + continue + + self._append_to_entity_list( project_name=project_name, list_entity=existing_list, version_ids=version_ids, ) + + # Else create the new list else: - entity_list_folder_id = None # make sure parent folder exists - if parent_folders: - entity_list_folder_id = self._get_or_create_parent_folder( - project_name=project_name, - parent_folders=parent_folders, - ) + entity_list_folder_id = self._get_or_create_parent_folder( + project_name=project_name, + parent_folders=list_config.parent_folders, + ) # then create list under a parent folder self._create_entity_list( - list_type=list_data["list_type"], project_name=project_name, entity_type="version", - label=list_label, + label=list_config.name, + list_type=list_config.list_type, entity_list_folder_id=entity_list_folder_id, items=[ {"entityId": version_id} @@ -115,73 +203,44 @@ def process(self, context): def _get_or_create_parent_folder( self, project_name: str, - parent_folders: list[str], + parent_folders: list[str] | None, ) -> str | None: + # TODO: Should we apply 'data.scopes' to the folder to make the folder + # list only under e.g. generic list or review-sessions lists instead + # of scoped to both. if not parent_folders: return None - existing_list_folders = self._get_list_folders_helper( + + existing_list_folders = get_entity_list_folders( project_name=project_name ) parent_folder_id = None for folder_name in parent_folders: # make sure to get existing folder id if it exists # and use it instead of creating a new one - existing_list_folder_id = None for list_folder in existing_list_folders: - if list_folder["label"] == folder_name: - existing_list_folder_id = list_folder["id"] - break - if existing_list_folder_id: - parent_folder_id = existing_list_folder_id - continue - # if folder does not exist, create it - # under the existing parent folder - parent_folder_id = self._create_list_folder_helper( - project_name=project_name, - folder_name=folder_name, - parent_folder_id=parent_folder_id, - ) + if list_folder["parentId"] != parent_folder_id: + continue + if list_folder["label"] != folder_name: + continue + parent_folder_id = list_folder["id"] + break + else: + # if folder does not exist, create it under the parent folder + parent_folder_id = create_entity_list_folder( + project_name=project_name, + folder_name=folder_name, + parent_folder_id=parent_folder_id, + ) return parent_folder_id - # TODO: ayon_api future compatibility, remove once ayon_api supports it - def _create_list_folder_helper( - self, - project_name: str, - folder_name: str, - parent_folder_id: str | None = None, - ) -> str: - kwargs = { - "id": create_entity_id(), - "label": folder_name, - } - if parent_folder_id: - kwargs["parentId"] = parent_folder_id - response = ayon_api.post( - f"projects/{project_name}/entityListFolders", - **kwargs, - ) - response.raise_for_status() - self.log.debug(f"List folder created: {response.data}") - return response.data["id"] - - # TODO: ayon_api future compatibility, remove once ayon_api supports it - def _get_list_folders_helper( - self, - project_name: str, - ) -> list[dict]: - response = ayon_api.get( - f"projects/{project_name}/entityListFolders", - ) - response.raise_for_status() - self.log.debug(f"List folders: {response.data}") - return response.data["folders"] - - def _update_entity_list( + def _append_to_entity_list( self, project_name: str, list_entity: dict, version_ids: list[str] | None = None, ): + """Append version ids to the entity list as items.""" if version_ids is None: return @@ -205,6 +264,7 @@ def _create_entity_list( list_type: str | None = None, entity_list_folder_id: str | None = None, ) -> dict: + """Create a new list parented to the entity list folder.""" kwargs = { "id": create_entity_id(), "entityType": entity_type, @@ -226,50 +286,3 @@ def _create_entity_list( response.raise_for_status() self.log.debug(f"Entity list created: {response.data}") return response.data - - @staticmethod - def _existing_list( - all_list_entities: Generator[dict[str, Any], None, None], - list_label: str, - list_type: str, - ) -> dict | None: - - # iterate via all lists and check if the label matches - for list_data in all_list_entities: - if list_data["entityListType"] != list_type: - continue - if list_data["label"] != list_label: - continue - return list_data - return None - - -# TODO: ayon_api future compatibility, remove once ayon_api supports it -def create_entity_list_item( - project_name: str, - list_id: str, - entity_id: str, -) -> str: - """Create entity list item. - - Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id where item will be added. - entity_id (str): Id of entity added to the list. - - Returns: - str: Item id. - - """ - item_id = create_entity_id() - kwargs = { - "id": item_id, - "entityId": entity_id, - } - - response = ayon_api.post( - f"projects/{project_name}/lists/{list_id}/items", - **kwargs - ) - response.raise_for_status() - return item_id From 8f3d59085008cac8e15e23da6d95e143ea16ec4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 4 Jun 2026 11:12:28 +0200 Subject: [PATCH 16/51] Update server/settings/publish_plugins.py Co-authored-by: Roy Nieterau --- server/settings/publish_plugins.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 18651e6a058..b2c7809ddb4 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -270,9 +270,6 @@ class CollectVersionToListModel(BaseSettingsModel): profiles: list[CollectVersionToListProfileModel] = SettingsField( default_factory=list, title="Profiles", - description=( - "" - ) ) From b0cdc8d74f9d8edd10d67d7b6f0d5b6911bfb167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 4 Jun 2026 11:14:36 +0200 Subject: [PATCH 17/51] Update server/settings/publish_plugins.py Co-authored-by: Roy Nieterau --- server/settings/publish_plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index b2c7809ddb4..eb9b77224ce 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -245,9 +245,9 @@ class CollectVersionToListProfileModel(BaseSettingsModel): title="Product names", description="The product names to match this profile to.", ) - name_template: str = SettingsField( + name: str = SettingsField( "", - title="Name template", + title="Name", description="Anatomy formattable template for the name.", section="List configuration", ) From 26521b91d8398c96e394af5a71c1a0e4e8106f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 4 Jun 2026 11:15:16 +0200 Subject: [PATCH 18/51] Update server/settings/publish_plugins.py Co-authored-by: Roy Nieterau --- server/settings/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index eb9b77224ce..db6edf0498a 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1506,7 +1506,7 @@ class PublishPuginsModel(BaseSettingsModel): ] }, "CollectVersionToList": { - "enabled": True, + "enabled": False, "profiles": [] }, "CollectExplicitResolution": { From 87284a9514e3a260779c5b24e2ac07793945ba32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 4 Jun 2026 11:15:26 +0200 Subject: [PATCH 19/51] Update server/settings/publish_plugins.py Co-authored-by: Roy Nieterau --- server/settings/publish_plugins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index db6edf0498a..8f0219ba151 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -261,7 +261,6 @@ class CollectVersionToListProfileModel(BaseSettingsModel): title="List type", description="Define what type of list this profile represents.", enum_resolver=list_type_enum, - ) From 55ea43fe09118615304a7de3cc4d0ecb2231da68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 4 Jun 2026 11:13:09 +0200 Subject: [PATCH 20/51] Remove unused imports from typing Remove unused imports from typing The `Any` and `Generator` imports are no longer used in this module. --- client/ayon_core/plugins/publish/integrate_versions_to_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 4f1bbd3fe04..dd059b6e4f9 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -10,7 +10,7 @@ import platform from copy import deepcopy from collections import defaultdict -from typing import TYPE_CHECKING, Any, Generator +from typing import TYPE_CHECKING import ayon_api import pyblish.api From 02daacfc298b9502e9017a1c065be8d50340a9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 4 Jun 2026 11:19:52 +0200 Subject: [PATCH 21/51] Use "name" instead of "name_template" in profile --- client/ayon_core/plugins/publish/collect_version_to_list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index fccbb5ade18..31698ba35d6 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -25,10 +25,10 @@ def process(self, instance): self.log.debug(f"Version lists already collected: {version_lists}") return - name_template = profile["name_template"] + name = profile["name"] version_lists.append( ListConfig( - name=name_template, + name=name, parent_folders=profile.get("parent_folders", None), list_type=profile["list_type"], ) From bdb41ed04fbbcdb3c0f2a14d291c8806cf167809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 4 Jun 2026 11:33:15 +0200 Subject: [PATCH 22/51] Fix version list profile key from name to list_name --- .../ayon_core/plugins/publish/collect_version_to_list.py | 2 +- .../plugins/publish/integrate_versions_to_list.py | 4 ++-- server/settings/publish_plugins.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index 31698ba35d6..df340d0afca 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -25,7 +25,7 @@ def process(self, instance): self.log.debug(f"Version lists already collected: {version_lists}") return - name = profile["name"] + name = profile["list_name"] version_lists.append( ListConfig( name=name, diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index dd059b6e4f9..1f265bec12f 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -8,18 +8,18 @@ from __future__ import annotations import platform -from copy import deepcopy from collections import defaultdict +from copy import deepcopy from typing import TYPE_CHECKING import ayon_api import pyblish.api from ayon_api.utils import create_entity_id from ayon_core.lib import StringTemplate +from ayon_core.pipeline.structures import ListConfig if TYPE_CHECKING: from ayon_core.pipeline import Anatomy - from ayon_core.pipeline.structures import ListConfig # TODO: ayon_api future compatibility, remove once ayon_api supports it diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 8f0219ba151..5b98b0a6128 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -208,7 +208,7 @@ def list_type_enum(): ] class CollectVersionToListProfileModel(BaseSettingsModel): - """.""" + """Collect version list profile model.""" _layout = "expanded" host_names: list[str] = SettingsField( default_factory=list, @@ -245,9 +245,9 @@ class CollectVersionToListProfileModel(BaseSettingsModel): title="Product names", description="The product names to match this profile to.", ) - name: str = SettingsField( - "", - title="Name", + list_name: str = SettingsField( + "{folder[name]}", + title="List Name", description="Anatomy formattable template for the name.", section="List configuration", ) From 20c5849780392fd77424bc5f7d8697eee9826aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 4 Jun 2026 14:45:47 +0200 Subject: [PATCH 23/51] Add blank lines before class definitions --- client/ayon_core/pipeline/structures.py | 1 + server/settings/publish_plugins.py | 1 + 2 files changed, 2 insertions(+) diff --git a/client/ayon_core/pipeline/structures.py b/client/ayon_core/pipeline/structures.py index d4669ae312a..b60fba015f0 100644 --- a/client/ayon_core/pipeline/structures.py +++ b/client/ayon_core/pipeline/structures.py @@ -5,6 +5,7 @@ ListType = Literal["generic", "review-session"] + @dataclass class ListConfig: """Define a list.""" diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 5b98b0a6128..fe6a42c7a50 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -207,6 +207,7 @@ def list_type_enum(): {"label": "Review Session", "value": "review-session"}, ] + class CollectVersionToListProfileModel(BaseSettingsModel): """Collect version list profile model.""" _layout = "expanded" From ea062c2b9a9b8d170bfc5ba8b1cd2214f8382ccf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 4 Jun 2026 23:14:24 +0200 Subject: [PATCH 24/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/plugins/publish/collect_version_to_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index df340d0afca..de3484d2f3c 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -10,6 +10,7 @@ class CollectVersionToList(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.499 label = "Collect Version to List" + settings_category = "core" profiles = [] From 69b1c71762ddcbba0d87ded1458ee709312d3ca7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 4 Jun 2026 23:15:51 +0200 Subject: [PATCH 25/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/plugins/publish/integrate_versions_to_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 1f265bec12f..a04ffa10575 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -95,7 +95,7 @@ class IntegrateVersionToList(pyblish.api.ContextPlugin): label = "Integrate Versions to List" order = pyblish.api.IntegratorOrder + 0.49 - + settings_category = "core" def process(self, context): list_config_by_list_name: dict[str, ListConfig] = {} version_ids_by_list_name: dict[str, list[str]] = defaultdict(list) From 1f8ede4b883027ec83e74b75e57f103172e94610 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 5 Jun 2026 00:24:06 +0200 Subject: [PATCH 26/51] Add Extract Thumbnail to workflow host --- client/ayon_core/plugins/publish/extract_thumbnail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 6e560d86d77..dd72f081cf4 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -110,6 +110,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "unreal", "houdini", "batchdelivery", + "workflow", ] settings_category = "core" enabled = False From 54caab18124cca504948cdbbcb6167bb223fc640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 09:56:06 +0200 Subject: [PATCH 27/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../publish/integrate_versions_to_list.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index a04ffa10575..75039785d4c 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -114,11 +114,19 @@ def process(self, context): "root": anatomy.roots, "platform": platform.system().lower(), }) - list_name: str = str( - StringTemplate.format_strict_template( - list_config.name, template_keys + try: + list_name: str = str( + StringTemplate.format_strict_template( + list_config.name, template_keys + ) ) - ) + except Exception: + self.log.error( + "Failed to format entity list name template: " + f"{list_config.name}", + exc_info=True, + ) + continue parent_folders: list[str] | None if parent_folders := list_config.parent_folders: parent_folders = [ From df29668cd0df48cdbf32cf4a6da8bd4542d9ddec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 10:01:50 +0200 Subject: [PATCH 28/51] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../publish/integrate_versions_to_list.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 75039785d4c..8ca38843064 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -137,12 +137,28 @@ def process(self, context): for folder in parent_folders ] - # Define the list config, and add the version id to the list - list_config_by_list_name[list_name] = ListConfig( + candidate_config = ListConfig( name=list_name, parent_folders=parent_folders, list_type=list_config.list_type, ) + + existing_config = list_config_by_list_name.get(list_name) + if existing_config: + if existing_config.list_type != candidate_config.list_type: + self.log.error( + "Conflicting list_type for entity list label " + f"'{list_name}': '{existing_config.list_type}' vs " + f"'{candidate_config.list_type}'. Skipping." + ) + continue + if (existing_config.parent_folders or None) != (candidate_config.parent_folders or None): + self.log.warning( + "Multiple parent_folders configured for entity list " + f"'{list_name}'. Using the first: {existing_config.parent_folders}" + ) + else: + list_config_by_list_name[list_name] = candidate_config version_ids_by_list_name[list_name].append( version_entity["id"] ) From dc9cc71f7832b0bfedc4ec924299ba970faab850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 10:05:22 +0200 Subject: [PATCH 29/51] Reformat long lines for readability --- .../plugins/publish/integrate_versions_to_list.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 8ca38843064..7d74bc44a28 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -152,10 +152,15 @@ def process(self, context): f"'{candidate_config.list_type}'. Skipping." ) continue - if (existing_config.parent_folders or None) != (candidate_config.parent_folders or None): + if ( + existing_config.parent_folders or None + ) != ( + candidate_config.parent_folders or None + ): self.log.warning( - "Multiple parent_folders configured for entity list " - f"'{list_name}'. Using the first: {existing_config.parent_folders}" + "Multiple parent_folders configured for " + f"entity list '{list_name}'. Using the " + f"first: {existing_config.parent_folders}" ) else: list_config_by_list_name[list_name] = candidate_config From 35cd60e361886d27b8847295081373640ee27f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 10:06:33 +0200 Subject: [PATCH 30/51] Add blank line before process method --- client/ayon_core/plugins/publish/integrate_versions_to_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 7d74bc44a28..8e4c4876b07 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -96,6 +96,7 @@ class IntegrateVersionToList(pyblish.api.ContextPlugin): label = "Integrate Versions to List" order = pyblish.api.IntegratorOrder + 0.49 settings_category = "core" + def process(self, context): list_config_by_list_name: dict[str, ListConfig] = {} version_ids_by_list_name: dict[str, list[str]] = defaultdict(list) From 3dca38c8795f58a1c5c3e4f841e2a015adf7f28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 10:08:26 +0200 Subject: [PATCH 31/51] Update client/ayon_core/plugins/publish/collect_version_to_list.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/collect_version_to_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index de3484d2f3c..11e8672279b 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -30,7 +30,7 @@ def process(self, instance): version_lists.append( ListConfig( name=name, - parent_folders=profile.get("parent_folders", None), + parent_folders=profile["parent_folders"], list_type=profile["list_type"], ) ) From 294bd93693070047fb88318a6f473d20a6bb6d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 10:11:41 +0200 Subject: [PATCH 32/51] Refactor version list initialization logic Move the existing version lists check before profile lookup to exit early if lists are already present, avoiding unnecessary profile retrieval. --- .../plugins/publish/collect_version_to_list.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index de3484d2f3c..e735006c6eb 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -15,16 +15,18 @@ class CollectVersionToList(pyblish.api.InstancePlugin): profiles = [] def process(self, instance): + if "versionLists" in instance.data: + version_lists = instance.data["versionLists"] + self.log.debug(f"Version lists already collected: {version_lists}") + return + + version_lists: list[ListConfig] = [] + instance.data["versionLists"] = version_lists + profile = self._get_profile_for_instance(instance) if not profile: self.log.debug(f"No profile found for instance {instance}") return - version_lists: list[ListConfig] = instance.data.setdefault( - "versionLists", []) - - if version_lists: - self.log.debug(f"Version lists already collected: {version_lists}") - return name = profile["list_name"] version_lists.append( From 2f6c9e78c41989f5ac128c1c848866c559e9cc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 10:40:06 +0200 Subject: [PATCH 33/51] Import field from dataclasses and use default_factory The default_factory approach is more explicit and avoids potential issues with mutable defaults. --- client/ayon_core/pipeline/structures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/structures.py b/client/ayon_core/pipeline/structures.py index b60fba015f0..2ff5709d4ba 100644 --- a/client/ayon_core/pipeline/structures.py +++ b/client/ayon_core/pipeline/structures.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Literal ListType = Literal["generic", "review-session"] @@ -10,5 +10,5 @@ class ListConfig: """Define a list.""" name: str - parent_folders: list[str] | None = None + parent_folders: list[str] = field(default_factory=list) list_type: ListType = "generic" From 3a07aa03a01593156e2ca35e8abbda430a81b2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 10:41:05 +0200 Subject: [PATCH 34/51] Fix version list retrieval and rename template variable - Add early continue if version_lists is empty to avoid redundant checks - Rename template_keys to template_data for clarity since it contains data for template formatting, not just keys --- .../plugins/publish/integrate_versions_to_list.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 8e4c4876b07..721ac13b3c4 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -102,7 +102,10 @@ def process(self, context): version_ids_by_list_name: dict[str, list[str]] = defaultdict(list) for instance in context: version_lists: list[ListConfig] = instance.data.get( - "versionLists", []) + "versionLists" + ) + if not version_lists: + continue version_entity = instance.data.get("versionEntity") if not version_entity or not version_lists: continue @@ -110,15 +113,15 @@ def process(self, context): # Construct the list config with the formatted name and parent # folder names anatomy: Anatomy = instance.context.data["anatomy"] - template_keys = deepcopy(instance.data["anatomyData"]) - template_keys.update({ + template_data = deepcopy(instance.data["anatomyData"]) + template_data.update({ "root": anatomy.roots, "platform": platform.system().lower(), }) try: list_name: str = str( StringTemplate.format_strict_template( - list_config.name, template_keys + list_config.name, template_data ) ) except Exception: @@ -133,7 +136,7 @@ def process(self, context): parent_folders = [ str(StringTemplate.format_template( folder, - template_keys + template_data )) for folder in parent_folders ] From d6778678c2bfc1faee4a73724fc7c6431f506367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 10:41:56 +0200 Subject: [PATCH 35/51] Set hasWebreview flag and skip review sessions without it When a review is successfully uploaded, mark the instance with hasWebreview. Skip adding versions to review-session lists if no webreview was created for that instance. --- client/ayon_core/plugins/publish/integrate_review.py | 1 + .../ayon_core/plugins/publish/integrate_versions_to_list.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index 49ea1b4c6cb..5f0dac0900d 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -77,6 +77,7 @@ def _upload_reviewable(self, project_name, version_id, instance): # Pass headers to fix bug in ayon-api (fixed in 1.2.15) headers={}, ) + instance.data["hasWebreview"] = True except Exception as exc: self.log.warning( f"Review upload failed after {time.time() - start}s.", diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 721ac13b3c4..d5630a2c5e2 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -101,6 +101,7 @@ def process(self, context): list_config_by_list_name: dict[str, ListConfig] = {} version_ids_by_list_name: dict[str, list[str]] = defaultdict(list) for instance in context: + has_webreview = instance.data.get("hasWebreview", False) version_lists: list[ListConfig] = instance.data.get( "versionLists" ) @@ -110,6 +111,11 @@ def process(self, context): if not version_entity or not version_lists: continue for list_config in version_lists: + if ( + list_config.list_type == "review-session" + and has_webreview is False + ): + continue # Construct the list config with the formatted name and parent # folder names anatomy: Anatomy = instance.context.data["anatomy"] From c1783006f634ab74bafc4f4118dda2f0f292f85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 10:47:24 +0200 Subject: [PATCH 36/51] returning type correct list output --- client/ayon_core/plugins/publish/integrate_versions_to_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index d5630a2c5e2..f1bef3980ac 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -103,7 +103,7 @@ def process(self, context): for instance in context: has_webreview = instance.data.get("hasWebreview", False) version_lists: list[ListConfig] = instance.data.get( - "versionLists" + "versionLists", [] ) if not version_lists: continue From fed8d543d287af6f06e89341b4e703a379039164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 11:00:46 +0200 Subject: [PATCH 37/51] Update client/ayon_core/plugins/publish/collect_version_to_list.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/collect_version_to_list.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index 69c7d5fb434..7848b98e0c3 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -28,10 +28,9 @@ def process(self, instance): self.log.debug(f"No profile found for instance {instance}") return - name = profile["list_name"] version_lists.append( ListConfig( - name=name, + name=profile["list_name"], parent_folders=profile["parent_folders"], list_type=profile["list_type"], ) From f4021f0869c4203e0f078fcce0ca14bbd99db5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 11:01:05 +0200 Subject: [PATCH 38/51] Update client/ayon_core/plugins/publish/integrate_versions_to_list.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../ayon_core/plugins/publish/integrate_versions_to_list.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index f1bef3980ac..b6dabc9233a 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -163,9 +163,8 @@ def process(self, context): ) continue if ( - existing_config.parent_folders or None - ) != ( - candidate_config.parent_folders or None + existing_config.parent_folders + != candidate_config.parent_folders ): self.log.warning( "Multiple parent_folders configured for " From 351c52ff0ce80585b2b37294fdd6de48dafd042f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 11:19:56 +0200 Subject: [PATCH 39/51] Add early return when no list configs found --- client/ayon_core/plugins/publish/integrate_versions_to_list.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index b6dabc9233a..bc53058b8cf 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -177,6 +177,9 @@ def process(self, context): version_entity["id"] ) + if not list_config_by_list_name: + return + # Get all list entities for the project from server project_name: str = context.data["projectName"] existing_list_entities_by_label: dict[str, dict] = { From 563af8f3ddfb06ccf428d9a2c7a2a0e81b19eb44 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:17:05 +0200 Subject: [PATCH 40/51] rename parent_folders to list_folders --- client/ayon_core/pipeline/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/structures.py b/client/ayon_core/pipeline/structures.py index 2ff5709d4ba..74915e2b658 100644 --- a/client/ayon_core/pipeline/structures.py +++ b/client/ayon_core/pipeline/structures.py @@ -10,5 +10,5 @@ class ListConfig: """Define a list.""" name: str - parent_folders: list[str] = field(default_factory=list) list_type: ListType = "generic" + list_folders: list[str] = field(default_factory=list) From f2da120552bd5c47198f5146a80329abd45e4f7d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:17:18 +0200 Subject: [PATCH 41/51] change list_folders to list of dataclasses --- client/ayon_core/pipeline/structures.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/structures.py b/client/ayon_core/pipeline/structures.py index 74915e2b658..46f76b7023f 100644 --- a/client/ayon_core/pipeline/structures.py +++ b/client/ayon_core/pipeline/structures.py @@ -6,9 +6,17 @@ ListType = Literal["generic", "review-session"] +@dataclass +class ListConfigFolder: + label: str + scope: list[str] = field(default_factory=list) + color: str | None = None + icon: str | None = None + + @dataclass class ListConfig: """Define a list.""" name: str list_type: ListType = "generic" - list_folders: list[str] = field(default_factory=list) + list_folders: list[ListConfigFolder] = field(default_factory=list) From f15b2e9c3a83f0e577dec0c15d848018d4011423 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:20:31 +0200 Subject: [PATCH 42/51] collect plugin does collect folders dataclasses --- .../ayon_core/plugins/publish/collect_version_to_list.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index 7848b98e0c3..23a2efe5f95 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -2,7 +2,7 @@ import pyblish.api from ayon_core.lib import filter_profiles -from ayon_core.pipeline.structures import ListConfig +from ayon_core.pipeline.structures import ListConfig, ListConfigFolder class CollectVersionToList(pyblish.api.InstancePlugin): @@ -31,8 +31,11 @@ def process(self, instance): version_lists.append( ListConfig( name=profile["list_name"], - parent_folders=profile["parent_folders"], list_type=profile["list_type"], + list_folders=[ + ListConfigFolder(label=name) + for name in profile["parent_folders"] + ], ) ) self.log.debug(f"Collected version lists: {version_lists}") From 0b024029f16b6010a6d28ef14856c3588179d09c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:44:49 +0200 Subject: [PATCH 43/51] change scope type hint --- client/ayon_core/pipeline/structures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/structures.py b/client/ayon_core/pipeline/structures.py index 46f76b7023f..6b976839ff9 100644 --- a/client/ayon_core/pipeline/structures.py +++ b/client/ayon_core/pipeline/structures.py @@ -9,7 +9,8 @@ @dataclass class ListConfigFolder: label: str - scope: list[str] = field(default_factory=list) + # Empty list means the folder is visible for all list types + scope: list[ListType] = field(default_factory=list) color: str | None = None icon: str | None = None From eb9f70b50151294dd32bb906f8922c21b0150854 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:46:24 +0200 Subject: [PATCH 44/51] handle all possible cases in integration --- .../publish/integrate_versions_to_list.py | 209 +++++++++++------- 1 file changed, 132 insertions(+), 77 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index bc53058b8cf..7292c744925 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -16,10 +16,10 @@ import pyblish.api from ayon_api.utils import create_entity_id from ayon_core.lib import StringTemplate -from ayon_core.pipeline.structures import ListConfig if TYPE_CHECKING: from ayon_core.pipeline import Anatomy + from ayon_core.pipeline.structures import ListConfig, ListConfigFolder # TODO: ayon_api future compatibility, remove once ayon_api supports it @@ -53,16 +53,41 @@ def create_entity_list_item( return item_id +# TODO: ayon_api future compatibility, remove once ayon_api supports it +def update_entity_list_folder( + project_name: str, + folder_id: str, + data: dict, +) -> None: + response = ayon_api.patch( + f"projects/{project_name}/entityListFolders/{folder_id}", + data=data, + ) + response.raise_for_status() + + # TODO: ayon_api future compatibility, remove once ayon_api supports it def create_entity_list_folder( project_name: str, - folder_name: str, - parent_folder_id: str | None = None, + list_folder: ListConfigFolder, + parent_folder_id: str | None, ) -> str: + data = {} + for key, value in ( + ("color", list_folder.color), + ("icon", list_folder.icon), + ("scope", list_folder.scope), + ): + if value is not None: + data[key] = value + kwargs = { "id": create_entity_id(), - "label": folder_name, + "label": list_folder.label, } + if data: + kwargs["data"] = data + if parent_folder_id: kwargs["parentId"] = parent_folder_id response = ayon_api.post( @@ -100,25 +125,29 @@ class IntegrateVersionToList(pyblish.api.ContextPlugin): def process(self, context): list_config_by_list_name: dict[str, ListConfig] = {} version_ids_by_list_name: dict[str, list[str]] = defaultdict(list) + + anatomy: Anatomy = context.data["anatomy"] for instance in context: - has_webreview = instance.data.get("hasWebreview", False) version_lists: list[ListConfig] = instance.data.get( "versionLists", [] ) if not version_lists: continue + version_entity = instance.data.get("versionEntity") - if not version_entity or not version_lists: + if not version_entity: continue + + has_webreview = instance.data.get("hasWebreview", False) for list_config in version_lists: + # Skip review-session lists if instance does not have review if ( - list_config.list_type == "review-session" - and has_webreview is False + not has_webreview + and list_config.list_type == "review-session" ): continue # Construct the list config with the formatted name and parent # folder names - anatomy: Anatomy = instance.context.data["anatomy"] template_data = deepcopy(instance.data["anatomyData"]) template_data.update({ "root": anatomy.roots, @@ -132,51 +161,60 @@ def process(self, context): ) except Exception: self.log.error( - "Failed to format entity list name template: " + "Failed to fill entity list name template: " f"{list_config.name}", exc_info=True, ) continue - parent_folders: list[str] | None - if parent_folders := list_config.parent_folders: - parent_folders = [ - str(StringTemplate.format_template( - folder, - template_data - )) - for folder in parent_folders - ] - - candidate_config = ListConfig( - name=list_name, - parent_folders=parent_folders, - list_type=list_config.list_type, - ) existing_config = list_config_by_list_name.get(list_name) - if existing_config: - if existing_config.list_type != candidate_config.list_type: - self.log.error( - "Conflicting list_type for entity list label " - f"'{list_name}': '{existing_config.list_type}' vs " - f"'{candidate_config.list_type}'. Skipping." - ) - continue - if ( - existing_config.parent_folders - != candidate_config.parent_folders - ): - self.log.warning( - "Multiple parent_folders configured for " - f"entity list '{list_name}'. Using the " - f"first: {existing_config.parent_folders}" - ) - else: - list_config_by_list_name[list_name] = candidate_config + if ( + existing_config + and existing_config.list_type != list_config.list_type + ): + self.log.error( + "Conflicting list_type for entity list label " + f"'{list_name}': '{existing_config.list_type}' vs " + f"'{list_config.list_type}'. Skipping." + ) + continue + + # Add version to lists mapping version_ids_by_list_name[list_name].append( version_entity["id"] ) + # Fill label using template data + candidate_config = deepcopy(list_config) + for folder in list_config.list_folders: + folder.label = str(StringTemplate.format_template( + folder.label, template_data + )) + + # Use candadate config + if existing_config is None: + list_config_by_list_name[list_name] = candidate_config + continue + + # Compare folders of the existing config and the candidate + # config. Does not affect output but logs a warning if + # they differ. + existing_labels = [ + folder.label + for folder in existing_config.list_folders + ] + candidate_labels = [ + folder.label + for folder in candidate_config.list_folders + ] + if existing_labels != candidate_labels: + self.log.warning( + "Configuration does contain different folders from" + f" existing list '{list_name}'. Keeping existing list" + f" folders. Existing: {existing_labels}" + f" vs Candidate: {candidate_labels}" + ) + if not list_config_by_list_name: return @@ -184,10 +222,10 @@ def process(self, context): project_name: str = context.data["projectName"] existing_list_entities_by_label: dict[str, dict] = { entity_list["label"]: entity_list - for entity_list in ayon_api.get_entity_lists( - project_name=project_name - ) + for entity_list in ayon_api.get_entity_lists(project_name) } + existing_list_folders = get_entity_list_folders(project_name) + for list_name, list_config in list_config_by_list_name.items(): version_ids = version_ids_by_list_name[list_name] if not version_ids: @@ -224,9 +262,10 @@ def process(self, context): # Else create the new list else: # make sure parent folder exists - entity_list_folder_id = self._get_or_create_parent_folder( + entity_list_folder_id = self._create_list_folders( project_name=project_name, - parent_folders=list_config.parent_folders, + list_config=list_config, + existing_list_folders=existing_list_folders, ) # then create list under a parent folder self._create_entity_list( @@ -241,57 +280,73 @@ def process(self, context): ], ) - def _get_or_create_parent_folder( + def _create_list_folders( self, project_name: str, - parent_folders: list[str] | None, + list_config: ListConfig, + existing_list_folders: list[dict], ) -> str | None: - # TODO: Should we apply 'data.scopes' to the folder to make the folder - # list only under e.g. generic list or review-sessions lists instead - # of scoped to both. - if not parent_folders: + """Returns folder id of the last folder in the hierarchy.""" + if not list_config.list_folders: return None - existing_list_folders = get_entity_list_folders( - project_name=project_name - ) parent_folder_id = None - for folder_name in parent_folders: + for list_folder in list_config.list_folders: # make sure to get existing folder id if it exists # and use it instead of creating a new one - for list_folder in existing_list_folders: - if list_folder["parentId"] != parent_folder_id: - continue - if list_folder["label"] != folder_name: - continue - parent_folder_id = list_folder["id"] - break + for existing_list_folder in existing_list_folders: + if ( + existing_list_folder["parentId"] == parent_folder_id + and existing_list_folder["label"] == list_folder.label + ): + # If folder exists, check if scope needs to be updated + # with the list type. + # NOTE If scope is None or empty list it is scoped for + # everything. So the list type is added to scope only + # if scope exists and does not contain the list type. + scope = existing_list_folder["data"].get("scope") + if scope and list_config.list_type not in scope: + scope.append(list_config.list_type) + update_entity_list_folder( + project_name, + existing_list_folder["id"], + data={"scope": scope}, + ) + parent_folder_id = existing_list_folder["id"] + break else: # if folder does not exist, create it under the parent folder - parent_folder_id = create_entity_list_folder( + new_folder_id = create_entity_list_folder( project_name=project_name, - folder_name=folder_name, + list_folder=list_folder, parent_folder_id=parent_folder_id, ) + # Add new fake folder entity to existing folders + existing_list_folders.append({ + "label": list_folder.label, + "parentId": parent_folder_id, + "id": new_folder_id, + "data": { + "scope": list(list_folder.scope), + }, + }) + parent_folder_id = new_folder_id return parent_folder_id def _append_to_entity_list( self, project_name: str, list_entity: dict, - version_ids: list[str] | None = None, - ): + version_ids: list[str], + ) -> None: """Append version ids to the entity list as items.""" - if version_ids is None: - return - for entity_id in version_ids: item_id = create_entity_list_item( project_name=project_name, list_id=list_entity["id"], entity_id=entity_id, ) - self.log.info( + self.log.debug( f"Created entity list item {item_id} for version {entity_id}" f" in list {list_entity['label']}" ) @@ -302,8 +357,8 @@ def _create_entity_list( entity_type: str, label: str, items: list[dict], - list_type: str | None = None, - entity_list_folder_id: str | None = None, + list_type: str, + entity_list_folder_id: str | None, ) -> dict: """Create a new list parented to the entity list folder.""" kwargs = { @@ -311,9 +366,9 @@ def _create_entity_list( "entityType": entity_type, "label": label, "items": items, + "entityListType": list_type, } for key, value in ( - ("entityListType", list_type), ("entityListFolderId", entity_list_folder_id), ): if value is not None: From 38dbbdd7c8b01b678e33e5f77c11c62a74747ba2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:54:59 +0200 Subject: [PATCH 45/51] allow to define scope of folders in settings --- .../publish/collect_version_to_list.py | 18 ++++++++--- server/settings/publish_plugins.py | 32 ++++++++++++++++--- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index 23a2efe5f95..e8be68d7b75 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -28,14 +28,24 @@ def process(self, instance): self.log.debug(f"No profile found for instance {instance}") return + list_folders = [] + for item in profile["list_folders"]: + scope = [] + if item["scope_def"] == "list_type": + scope.append(profile["list_type"]) + + list_folders.append( + ListConfigFolder( + label=item["label"], + scope=scope, + ) + ) + version_lists.append( ListConfig( name=profile["list_name"], list_type=profile["list_type"], - list_folders=[ - ListConfigFolder(label=name) - for name in profile["parent_folders"] - ], + list_folders=list_folders, ) ) self.log.debug(f"Collected version lists: {version_lists}") diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index fe6a42c7a50..128638d7d35 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -208,6 +208,28 @@ def list_type_enum(): ] +def list_folder_scope_def(): + return [ + {"label": "Scope folder to all views", "value": "all"}, + {"label": "Scope to the list type", "value": "list_type"}, + ] + + +class VersionListFolderModel(BaseSettingsModel): + """Folder must have label and can be scoped to views. + + Scope of the folder can be defined for all views or use just the view + matching list type of created list. + + """ + label: str = SettingsField("", title="Folder label") + scope_def: str = SettingsField( + "all", + enum_resolver=list_folder_scope_def, + title="Scope", + ) + + class CollectVersionToListProfileModel(BaseSettingsModel): """Collect version list profile model.""" _layout = "expanded" @@ -252,17 +274,17 @@ class CollectVersionToListProfileModel(BaseSettingsModel): description="Anatomy formattable template for the name.", section="List configuration", ) - parent_folders: list[str] = SettingsField( - default_factory=list, - title="Parent folders", - description="Folder hierarchy formed from top to bottom.", - ) list_type: str = SettingsField( "generic", title="List type", description="Define what type of list this profile represents.", enum_resolver=list_type_enum, ) + list_folders: list[VersionListFolderModel] = SettingsField( + default_factory=list, + title="List folders", + description="Folder hierarchy formed from top to bottom.", + ) class CollectVersionToListModel(BaseSettingsModel): From 1f6d84559da2ea62862422525167215382aca6a5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:07:39 +0200 Subject: [PATCH 46/51] change layout --- server/settings/publish_plugins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 128638d7d35..e9b692dbc77 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -215,13 +215,14 @@ def list_folder_scope_def(): ] -class VersionListFolderModel(BaseSettingsModel): +class EntityListFolderModel(BaseSettingsModel): """Folder must have label and can be scoped to views. Scope of the folder can be defined for all views or use just the view matching list type of created list. """ + _layout = "expanded" label: str = SettingsField("", title="Folder label") scope_def: str = SettingsField( "all", @@ -280,7 +281,7 @@ class CollectVersionToListProfileModel(BaseSettingsModel): description="Define what type of list this profile represents.", enum_resolver=list_type_enum, ) - list_folders: list[VersionListFolderModel] = SettingsField( + list_folders: list[EntityListFolderModel] = SettingsField( default_factory=list, title="List folders", description="Folder hierarchy formed from top to bottom.", From a343264bccb40eab476ca334540d94ba9a8ef8f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:08:57 +0200 Subject: [PATCH 47/51] added some explanation to the settings --- server/settings/publish_plugins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index e9b692dbc77..3c04b5847c9 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -219,7 +219,9 @@ class EntityListFolderModel(BaseSettingsModel): """Folder must have label and can be scoped to views. Scope of the folder can be defined for all views or use just the view - matching list type of created list. + matching list type of created list. In case the list folder already + exists the settings are not used and we just make sure the list can + be seen under the folder. """ _layout = "expanded" @@ -232,7 +234,6 @@ class EntityListFolderModel(BaseSettingsModel): class CollectVersionToListProfileModel(BaseSettingsModel): - """Collect version list profile model.""" _layout = "expanded" host_names: list[str] = SettingsField( default_factory=list, From 718103d4cc8c3d0708193750282d5ef3f150e052 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:23:56 +0200 Subject: [PATCH 48/51] use label instead of name on list config --- client/ayon_core/pipeline/structures.py | 2 +- .../publish/collect_version_to_list.py | 2 +- .../publish/integrate_versions_to_list.py | 39 ++++++++++--------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/pipeline/structures.py b/client/ayon_core/pipeline/structures.py index 6b976839ff9..f7c03815f39 100644 --- a/client/ayon_core/pipeline/structures.py +++ b/client/ayon_core/pipeline/structures.py @@ -18,6 +18,6 @@ class ListConfigFolder: @dataclass class ListConfig: """Define a list.""" - name: str + label: str list_type: ListType = "generic" list_folders: list[ListConfigFolder] = field(default_factory=list) diff --git a/client/ayon_core/plugins/publish/collect_version_to_list.py b/client/ayon_core/plugins/publish/collect_version_to_list.py index e8be68d7b75..5421fd2c6f7 100644 --- a/client/ayon_core/plugins/publish/collect_version_to_list.py +++ b/client/ayon_core/plugins/publish/collect_version_to_list.py @@ -43,7 +43,7 @@ def process(self, instance): version_lists.append( ListConfig( - name=profile["list_name"], + label=profile["list_name"], list_type=profile["list_type"], list_folders=list_folders, ) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 7292c744925..2c77c3f71f5 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -123,8 +123,8 @@ class IntegrateVersionToList(pyblish.api.ContextPlugin): settings_category = "core" def process(self, context): - list_config_by_list_name: dict[str, ListConfig] = {} - version_ids_by_list_name: dict[str, list[str]] = defaultdict(list) + list_config_by_list_label: dict[str, ListConfig] = {} + version_ids_by_list_label: dict[str, list[str]] = defaultdict(list) anatomy: Anatomy = context.data["anatomy"] for instance in context: @@ -154,38 +154,39 @@ def process(self, context): "platform": platform.system().lower(), }) try: - list_name: str = str( + list_label: str = str( StringTemplate.format_strict_template( - list_config.name, template_data + list_config.label, template_data ) ) except Exception: self.log.error( "Failed to fill entity list name template: " - f"{list_config.name}", + f"{list_config.label}", exc_info=True, ) continue - existing_config = list_config_by_list_name.get(list_name) + existing_config = list_config_by_list_label.get(list_label) if ( existing_config and existing_config.list_type != list_config.list_type ): self.log.error( "Conflicting list_type for entity list label " - f"'{list_name}': '{existing_config.list_type}' vs " + f"'{list_label}': '{existing_config.list_type}' vs " f"'{list_config.list_type}'. Skipping." ) continue # Add version to lists mapping - version_ids_by_list_name[list_name].append( + version_ids_by_list_label[list_label].append( version_entity["id"] ) # Fill label using template data candidate_config = deepcopy(list_config) + candidate_config.label = list_label for folder in list_config.list_folders: folder.label = str(StringTemplate.format_template( folder.label, template_data @@ -193,7 +194,7 @@ def process(self, context): # Use candadate config if existing_config is None: - list_config_by_list_name[list_name] = candidate_config + list_config_by_list_label[list_label] = candidate_config continue # Compare folders of the existing config and the candidate @@ -210,12 +211,12 @@ def process(self, context): if existing_labels != candidate_labels: self.log.warning( "Configuration does contain different folders from" - f" existing list '{list_name}'. Keeping existing list" + f" existing list '{list_label}'. Keeping existing list" f" folders. Existing: {existing_labels}" f" vs Candidate: {candidate_labels}" ) - if not list_config_by_list_name: + if not list_config_by_list_label: return # Get all list entities for the project from server @@ -226,21 +227,23 @@ def process(self, context): } existing_list_folders = get_entity_list_folders(project_name) - for list_name, list_config in list_config_by_list_name.items(): - version_ids = version_ids_by_list_name[list_name] + for list_config in list_config_by_list_label.values(): + version_ids = version_ids_by_list_label[list_config.label] if not version_ids: - self.log.debug(f"No version ids for list: {list_name}") + self.log.debug(f"No version ids for list: {list_config.label}") continue # If list exists, append to it but ensure it is of correct type - existing_list = existing_list_entities_by_label.get(list_name) + existing_list = existing_list_entities_by_label.get( + list_config.label + ) if existing_list: if existing_list["entityListType"] != list_config.list_type: entity_list_type = existing_list["entityListType"] self.log.error( "Can't add versions to list because another entity" f" list type '{entity_list_type}' with that label" - f" already exists: {list_config.name}" + f" already exists: {list_config.label}" ) continue @@ -249,7 +252,7 @@ def process(self, context): self.log.error( f"Can't add versions to list because a '{entity_type}'" " list type with that label already exists: " - f"{list_config.name}" + f"{list_config.label}" ) continue @@ -271,7 +274,7 @@ def process(self, context): self._create_entity_list( project_name=project_name, entity_type="version", - label=list_config.name, + label=list_config.label, list_type=list_config.list_type, entity_list_folder_id=entity_list_folder_id, items=[ From 5c7ed7628f24e74c320ed41a98e52e5628dc80ea Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:43:11 +0200 Subject: [PATCH 49/51] add comment to settings --- server/settings/publish_plugins.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 3c04b5847c9..3df6d5d8039 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -226,6 +226,8 @@ class EntityListFolderModel(BaseSettingsModel): """ _layout = "expanded" label: str = SettingsField("", title="Folder label") + # Don't use explicit scope enum, rather ask if the folder should be seen + # everywhere or just in the list type matching created list. scope_def: str = SettingsField( "all", enum_resolver=list_folder_scope_def, From 422c677137dc15f9b81809ce0ce69f321a31b38f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:43:39 +0200 Subject: [PATCH 50/51] added bypass for scope handling --- .../plugins/publish/integrate_versions_to_list.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/ayon_core/plugins/publish/integrate_versions_to_list.py b/client/ayon_core/plugins/publish/integrate_versions_to_list.py index 2c77c3f71f5..1c6c790fe32 100644 --- a/client/ayon_core/plugins/publish/integrate_versions_to_list.py +++ b/client/ayon_core/plugins/publish/integrate_versions_to_list.py @@ -310,6 +310,14 @@ def _create_list_folders( scope = existing_list_folder["data"].get("scope") if scope and list_config.list_type not in scope: scope.append(list_config.list_type) + # --- Temporary bypass --- + # TODO remove when fixed in frontend + # There is an issue with list folder having both + # 'generic' and 'review-session' in scope. The + # folder is not visible in Lists UI. + scope = [] + existing_list_folder["data"]["scope"] = scope + # ------------------------ update_entity_list_folder( project_name, existing_list_folder["id"], From f6cd9d3693773d6d4945a4f5ee883c1396b66f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 5 Jun 2026 15:56:51 +0200 Subject: [PATCH 51/51] Add description to EntityListFolderModel label field --- server/settings/publish_plugins.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 3df6d5d8039..7f0058afa3a 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -225,7 +225,14 @@ class EntityListFolderModel(BaseSettingsModel): """ _layout = "expanded" - label: str = SettingsField("", title="Folder label") + label: str = SettingsField( + "", + title="Folder label", + description=( + "The label of the folder to create. " + "Anatomy formattable template for the name." + ), + ) # Don't use explicit scope enum, rather ask if the folder should be seen # everywhere or just in the list type matching created list. scope_def: str = SettingsField(