Skip to content

Commit ccae094

Browse files
SG-43456 [Phases 1-2] Enable FlowAM integration (#137)
* Add MEDM abstractions and utilities * Apply black * Apply black - pre-commit version * Add remaining types * DRY * SG-43457 [Part2] Migration of MEDM main logic and resources (#138) * Migration of the MEDM resources * Migration of MEDM core logic * Fix thumbnail display on version history * Tentative wrap-up * Format * Fix CI: add missing fw * Update Azure pipelines additional_repositories * Revert framework additions for CI * Comment framework for CI * Test CI green * Add remaining types * Fix types * Code review feedback * Move `DRAFT_VERSION_IDENTIFIER` to constants * SG-43458 [Part3] Config Hooks (#139) * Migrate config hooks * Add type annotations * Wrap `get_am_base_obj` into the loader action manager * Wraps constant * SG-43460 [Part4] Build dialogs and template utils (#140) * Migrate `BuildAssetDialog` and `BuildTemplateDialog` classes * Migrate template helpers * Code review feedback * Update setting name * Change MEDM to FlowAM in comments * Replace MEDM in log messages * SG-43626 Fix bug when details panel collapses/expands (#142) * Fix bug when details panel collapses/expands * SG-43627 Fix empty space gap on the details panel (#143) * SG-43677 Fix hooks to support multiple kwargs (#144) * SG-43459 Migrate Build scene/template workflows (#145) * Remove framework dependency from dialogs classes * Migrate build asset/template workflows * Remove more references * Use sandbox functions * Update references on models * Migrate thumbnail utilities * Use more sandbox functions * Finish migrating framework-floam references * Update references from sgtk.flowam.create * Remove framework loading * Rename fucntion to get FlowAMActions instance * Rename `am_base_obj` to `flowam_actions` * Latest Updates * Finish clean-up * Update imports * Tested reference link and download workflows * Finished testing build scene, open, reference, discard. * Tested create template workflow * Add tk_core_ref value for testing * Format * Fix fixture * remove migrated methods * Rename `medm` directory to `flowam` * Removed `enable_flowam` setting * Get rid of `FlowAMActions` dependency injection and use app level module * Update core new definitions * Fix format * Handle error * Remove dead code * Update references, rename variables * Fix discard draft logic * Format * Update reference sequence * Improved `CreateReferenceError` * CR fixes * Fix method signature * Import flowam module just to be safe * Add codecov.yml file * Exclude ui files from codecov * Fix paths * Add more files * Add hosts validationo * feat: [SG-43419] Launch publisher via engine command with single_file_mode support (#147) Replace direct app lookup + non-existent _set_context() call with the established engine command callback pattern. Grab the publisher app from the matched command's properties to call show_dialog() directly, passing single_file_mode=True when the action is a Flow AM republish ("publish"), so the dialog restricts users to dropping or browsing a single file. * Try to add support to older cores * Fix format * Update conditionals * Point CI to tk-core master * Remove config dependency for entity mapping (#148) * Remove config dependency for entity mapping * Code review feedback * Actions reorg * Update workfile actions * Format! --------- Co-authored-by: Ming Chen <ming.chen@autodesk.com>
1 parent bd97c3d commit ccae094

57 files changed

Lines changed: 11926 additions & 19407 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
A loader application that lets you add new items to the scene.
1313
"""
1414

15+
from types import ModuleType
16+
1517
import sgtk
1618
import os
1719

@@ -28,6 +30,7 @@ def init_app(self):
2830
return
2931

3032
tk_multi_loader = self.import_module("tk_multi_loader")
33+
self._flowam = tk_multi_loader.flowam
3134

3235
# the manager class provides the interface for loading. We store a
3336
# reference to it to enable the create_loader_action_manager method exposed on
@@ -95,3 +98,28 @@ def create_loader_manager(self, bundle=None):
9598
:returns: A :class:`tk_multi_loader.LoaderManager` instance
9699
"""
97100
return self._manager_class(bundle or self)
101+
102+
@property
103+
def flowam_available(self) -> bool:
104+
"""
105+
Returns True if FlowAM integration is available in the running core.
106+
107+
:returns: True if FlowAM integration is available, False otherwise
108+
:rtype: bool
109+
"""
110+
return (
111+
getattr(self.context, "flow_project_id", None) is not None
112+
and getattr(sgtk.platform.current_engine(), "flow_host", None) is not None
113+
)
114+
115+
@property
116+
def flowam(self) -> ModuleType:
117+
"""
118+
Access to the FlowAM integration module for this app. This module provides
119+
drop-in replacements for the standard Shotgun-based Loader models and actions,
120+
backed by Flow Asset Management (FlowAM) instead of the ShotGrid REST API.
121+
122+
:returns: The FlowAM integration module for this app
123+
:rtype: :mod:`tk_multi_loader.flowam`
124+
"""
125+
return self._flowam

build_resources.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
- ui_src: resources
77
ui_files:
8+
- build_asset_dialog
9+
- build_template_dialog
810
- dialog
911
- open_publish_form
1012
- widget_publish_history

codecov.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
ignore:
2+
# flowam and other files not covered by unit tests
3+
- "**python/tk_multi_loader/flowam/*"
4+
- "**python/tk_multi_loader/build_asset_dialog.py*"
5+
- "**python/tk_multi_loader/build_template_dialog.py*"
6+
- "**python/tk_multi_loader/ui/build_asset_dialog.py*"
7+
- "**python/tk_multi_loader/ui/build_template_dialog.py*"
8+
- "**python/tk_multi_loader/model_hierarchy.py"
9+
- "**python/tk_multi_loader/delegate_publish_list.py"
10+
- "**python/tk_multi_loader/delegate_publish_thumb.py"

hooks/tk-desktop_actions.py

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
# Copyright (c) 2026 Shotgun Software Inc.
2+
#
3+
# CONFIDENTIAL AND PROPRIETARY
4+
#
5+
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
6+
# Source Code License included in this distribution package. See LICENSE.
7+
# By accessing, using, copying or modifying this work you indicate your
8+
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
9+
# not expressly granted therein are reserved by Shotgun Software Inc.
10+
11+
"""
12+
Hook that loads defines all the available actions, broken down by publish type.
13+
"""
14+
15+
import os
16+
from typing import Any
17+
18+
import sgtk
19+
from sgtk import TankError
20+
21+
HookBaseClass = sgtk.get_hook_baseclass()
22+
23+
24+
class DesktopActions(HookBaseClass):
25+
"""
26+
Stub implementation of the shell actions, used for testing.
27+
"""
28+
29+
def generate_actions(
30+
self,
31+
sg_publish_data: dict,
32+
actions: list,
33+
ui_area: str,
34+
) -> list:
35+
"""
36+
Return a list of action instances for a particular publish.
37+
This method is called each time a user clicks a publish somewhere in the UI.
38+
The data returned from this hook will be used to populate the actions menu for a publish.
39+
40+
The mapping between Publish types and actions are kept in a different place
41+
(in the configuration) so at the point when this hook is called, the loader app
42+
has already established *which* actions are appropriate for this object.
43+
44+
The hook should return at least one action for each item passed in via the
45+
actions parameter.
46+
47+
This method needs to return detailed data for those actions, in the form of a list
48+
of dictionaries, each with name, params, caption and description keys.
49+
50+
Because you are operating on a particular publish, you may tailor the output
51+
(caption, tooltip etc) to contain custom information suitable for this publish.
52+
53+
The ui_area parameter is a string and indicates where the publish is to be shown.
54+
- If it will be shown in the main browsing area, "main" is passed.
55+
- If it will be shown in the details area, "details" is passed.
56+
- If it will be shown in the history area, "history" is passed.
57+
58+
Please note that it is perfectly possible to create more than one action "instance" for
59+
an action! You can for example do scene introspection - if the action passed in
60+
is "character_attachment" you may for example scan the scene, figure out all the nodes
61+
where this object can be attached and return a list of action instances:
62+
"attach to left hand", "attach to right hand" etc. In this case, when more than
63+
one object is returned for an action, use the params key to pass additional
64+
data into the run_action hook.
65+
66+
:param sg_publish_data: Shotgun data dictionary with all the standard publish fields.
67+
:param actions: List of action strings which have been defined in the app configuration.
68+
:param ui_area: String denoting the UI Area (see above).
69+
:returns List of dictionaries, each with keys name, params, caption and description
70+
"""
71+
app = self.parent
72+
app.log_debug(
73+
"Generate actions called for UI element %s. "
74+
"Actions: %s. Publish Data: %s" % (ui_area, actions, sg_publish_data)
75+
)
76+
77+
action_instances = []
78+
79+
if app.flowam_available:
80+
flowam_actions = app.flowam.FlowAMActions()
81+
82+
if "download" in actions and sg_publish_data.get("type") == "PublishedFile":
83+
version_number = sg_publish_data.get("version_number")
84+
85+
if (
86+
version_number is not None
87+
and version_number != flowam_actions.DRAFT_VERSION_IDENTIFIER
88+
):
89+
action_instances.append(
90+
{
91+
"name": "download",
92+
"params": "Download 'params'",
93+
"caption": "Download",
94+
"description": "Downloads the published file to a user specified location.",
95+
}
96+
)
97+
98+
if "publish" in actions:
99+
# Show publish action only for published files (not drafts)
100+
# Drafts (version == -1) are not supported in generic workflow
101+
version_number = sg_publish_data.get("version_number")
102+
103+
if version_number is not None and version_number >= 0:
104+
action_instances.append(
105+
{
106+
"name": "publish",
107+
"params": "Publish 'params'",
108+
"caption": "Publish",
109+
"description": "Publish a new revision of this generic asset.",
110+
}
111+
)
112+
113+
if "create_generic_asset" in actions:
114+
action_instances.append(
115+
{
116+
"name": "create_generic_asset",
117+
"params": "Create Generic Asset 'params'",
118+
"caption": "Create Generic Asset",
119+
"description": "Executes Create Generic Asset.",
120+
}
121+
)
122+
123+
if (
124+
"reference_copy_link" in actions
125+
and sg_publish_data.get(
126+
"version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER
127+
)
128+
> flowam_actions.DRAFT_VERSION_IDENTIFIER
129+
):
130+
action_instances.append(
131+
{
132+
"name": "reference_copy_link",
133+
"params": None,
134+
"caption": "Copy Reference Link",
135+
"description": "This will copy the reference as a string to the clipboard",
136+
"multi_select": False,
137+
}
138+
)
139+
140+
return action_instances
141+
142+
def execute_multiple_actions(self, actions: list) -> None:
143+
"""
144+
Executes the specified action on a list of items.
145+
146+
The default implementation dispatches each item from ``actions`` to
147+
the ``execute_action`` method.
148+
149+
The ``actions`` is a list of dictionaries holding all the actions to execute.
150+
Each entry will have the following values:
151+
152+
name: Name of the action to execute
153+
sg_publish_data: Publish information coming from Shotgun
154+
params: Parameters passed down from the generate_actions hook.
155+
156+
.. note::
157+
This is the default entry point for the hook. It reuses the ``execute_action``
158+
method for backward compatibility with hooks written for the previous
159+
version of the loader.
160+
161+
.. note::
162+
The hook will stop applying the actions on the selection if an error
163+
is raised midway through.
164+
165+
:param list actions: Action dictionaries.
166+
"""
167+
# Helps to visually scope selections
168+
# Execute each action.
169+
for single_action in actions:
170+
name = single_action["name"]
171+
sg_publish_data = single_action["sg_publish_data"]
172+
params = single_action["params"]
173+
self.execute_action(name, params, sg_publish_data)
174+
175+
def execute_action(
176+
self,
177+
name: str,
178+
params: Any,
179+
sg_publish_data: dict,
180+
) -> None:
181+
"""
182+
Print out all actions. The data sent to this be method will
183+
represent one of the actions enumerated by the generate_actions method.
184+
185+
:param name: Action name string representing one of the items returned by generate_actions.
186+
:param params: Params data, as specified by generate_actions.
187+
:param sg_publish_data: Shotgun data dictionary with all the standard publish fields.
188+
:returns: No return value expected.
189+
"""
190+
app = self.parent
191+
app.log_debug(
192+
"Execute action called for action %s. "
193+
"Parameters: %s. Publish Data: %s" % (name, params, sg_publish_data)
194+
)
195+
flowam_actions = app.flowam.FlowAMActions()
196+
197+
if name == "create_generic_asset":
198+
# Right click a task the left panel
199+
self._launch_publisher(name, sg_publish_data)
200+
201+
elif name == "publish":
202+
# action for a single PublishedFile
203+
self._launch_publisher(name, sg_publish_data)
204+
205+
elif name == "reference_copy_link":
206+
flowam_actions._create_reference_copy_link(sg_publish_data)
207+
208+
elif name == "download":
209+
flowam_actions._download_asset_revision(sg_publish_data)
210+
211+
def action_mappings(self) -> dict:
212+
"""
213+
Returns the action mappings for the loader app.
214+
215+
:returns: Dictionary of action mappings.
216+
"""
217+
app = self.parent
218+
if app.flowam_available:
219+
return {
220+
"All": ["download"],
221+
"Maya Workfile": ["download", "reference_copy_link"],
222+
"Nuke Workfile": ["download", "reference_copy_link"],
223+
"Houdini Workfile": ["download", "reference_copy_link"],
224+
"Generic Workfile": ["download", "reference_copy_link", "publish"],
225+
}
226+
227+
return {}
228+
229+
def entity_mappings(self) -> dict:
230+
"""
231+
Returns the entity mappings for the loader app.
232+
233+
:returns: Dictionary of entity mappings.
234+
"""
235+
app = self.parent
236+
if app.flowam_available:
237+
return {
238+
"Project": ["create_generic_asset"],
239+
"Task": ["create_generic_asset"],
240+
}
241+
242+
return {}
243+
244+
##############################################################################################################
245+
# helper methods which can be subclassed in custom hooks to fine tune the behavior of things
246+
247+
def _launch_publisher(self, action_name: str, sg_publish_data: dict) -> None:
248+
"""
249+
Launches the publisher app in the context of the specified entity (task or project).
250+
:param str action_name: Action name that triggered the publisher launch.
251+
:param dict sg_publish_data: Shotgun data dictionary with all the standard publish fields.
252+
"""
253+
engine = sgtk.platform.current_engine()
254+
255+
entity_type = sg_publish_data.get("type")
256+
if not entity_type:
257+
raise TankError("sg_publish_data missing 'type' field")
258+
259+
if entity_type == "Task":
260+
# Case when action is triggered from a Task item
261+
# as with "Create generic asset" action
262+
task_id = sg_publish_data["id"]
263+
entity_id = task_id
264+
elif entity_type == "Project":
265+
# Case when action is triggered from a Project item
266+
# as with "Create generic asset" action
267+
task_id = None # in this case task id is not relevant
268+
entity_id = sg_publish_data["id"]
269+
elif sg_publish_data.get("task"):
270+
# Case when action is triggered from a PublishedFile item
271+
# from task level, as with "Publish" action
272+
task_id = sg_publish_data["task"]["id"]
273+
entity_type = "Task"
274+
entity_id = task_id
275+
elif sg_publish_data.get("project"):
276+
# Case when action is trigger from a PublishedFile item
277+
# from project level, as with "Publish" action
278+
task_id = None # in this case task id is not relevant
279+
entity_type = "Project"
280+
entity_id = sg_publish_data["project"]["id"]
281+
else:
282+
raise TankError(f"Invalid entity type for publish: {entity_type}.")
283+
284+
# Use different env var naming for project-level vs task-level contexts
285+
if task_id is not None:
286+
revision_id_env_var = f"TK_FLOWAM_REVISION_ID_{task_id}"
287+
else:
288+
# For project-level contexts, use project ID
289+
project_id = entity_id
290+
revision_id_env_var = f"TK_FLOWAM_REVISION_ID_PROJECT_{project_id}"
291+
292+
if action_name == "publish":
293+
revision_id = sg_publish_data.get("sg_flow_revision_id")
294+
os.environ[revision_id_env_var] = revision_id
295+
else:
296+
# Clear possible previously existing publish states from an unfinished publish
297+
# (Finished publishes should clear this value)
298+
if revision_id_env_var in os.environ:
299+
os.environ.pop(revision_id_env_var)
300+
301+
# NOTE: the context should be either a Task or a Project
302+
entity_ctx = engine.tank.context_from_entity(entity_type, entity_id)
303+
304+
# Launch Publisher via its registered engine command callback.
305+
for cmd_settings in engine.commands.values():
306+
if cmd_settings.get("properties", {}).get("app") is None:
307+
continue
308+
if cmd_settings["properties"]["app"].name == "tk-multi-publish2":
309+
publisher_app = cmd_settings["properties"]["app"]
310+
single_file_mode = (
311+
action_name == "publish"
312+
and publisher_app.context.flow_project_id is not None
313+
)
314+
publisher_app.import_module("tk_multi_publish2").show_dialog(
315+
publisher_app, single_file_mode=single_file_mode
316+
)
317+
break
318+
else:
319+
raise TankError(
320+
"Could not find Publisher app (tk-multi-publish2)!\n\n"
321+
"Please ensure tk-multi-publish2 is configured in "
322+
"env/includes/desktop/project.yml under 'desktop.project: apps:'"
323+
)

0 commit comments

Comments
 (0)