diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 58447a8389..fb080d158b 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -321,7 +321,7 @@ def _check_submit_validations(self): return False if ( - not self._user_values.new_folder_name + self._user_values.new_folder_name is None and not self._selection_model.get_selected_folder_id() ): return False diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index ba603699bc..6bd4279219 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -26,7 +26,7 @@ from ayon_core.pipeline.version_start import get_versioning_start from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.publish import get_publish_template_name -from ayon_core.pipeline.create import get_product_name +from ayon_core.pipeline.create import get_product_name, TaskNotSetError UNKNOWN = object() @@ -823,15 +823,25 @@ def _determine_product_name(self): task_name = task_info["name"] task_type = task_info["taskType"] - product_name = get_product_name( - self._item.dst_project_name, - task_name, - task_type, - self.host_name, - product_type, - self._item.variant, - project_settings=self._project_settings - ) + try: + product_name = get_product_name( + self._item.dst_project_name, + task_name, + task_type, + self.host_name, + product_type, + self._item.variant, + project_settings=self._project_settings + ) + except TaskNotSetError: + self._status.set_failed( + "Target product name template requires task name. To continue" + " you have to select target task or change settings" + " ayon+settings://core/tools/creator/product_name_profiles" + f"?project={self._item.dst_project_name}." + ) + raise PushToProjectError(self._status.fail_reason) + self._log_info( f"Push will be integrating to product with name '{product_name}'" ) diff --git a/client/ayon_core/tools/push_to_project/models/user_values.py b/client/ayon_core/tools/push_to_project/models/user_values.py index edef2fe4fb..e52cb2917c 100644 --- a/client/ayon_core/tools/push_to_project/models/user_values.py +++ b/client/ayon_core/tools/push_to_project/models/user_values.py @@ -84,8 +84,11 @@ def set_new_folder_name(self, folder_name): return self._new_folder_name = folder_name - is_valid = True - if folder_name: + if folder_name is None: + is_valid = True + elif not folder_name: + is_valid = False + else: is_valid = ( self.folder_name_regex.match(folder_name) is not None ) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 4d64509afd..a69c512fcd 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -8,12 +8,69 @@ ProjectsCombobox, FoldersWidget, TasksWidget, + NiceCheckbox, ) from ayon_core.tools.push_to_project.control import ( PushToContextController, ) +class ErrorDetailDialog(QtWidgets.QDialog): + def __init__(self, parent): + super().__init__(parent) + + self.setWindowTitle("Error detail") + self.setWindowIcon(QtGui.QIcon(get_app_icon_path())) + + title_label = QtWidgets.QLabel(self) + + sep_1 = SeparatorWidget(parent=self) + + detail_widget = QtWidgets.QTextBrowser(self) + detail_widget.setReadOnly(True) + detail_widget.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + + sep_2 = SeparatorWidget(parent=self) + + btns_widget = QtWidgets.QWidget(self) + + copy_btn = QtWidgets.QPushButton("Copy", btns_widget) + close_btn = QtWidgets.QPushButton("Close", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(copy_btn, 0) + btns_layout.addWidget(close_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.addWidget(title_label, 0) + main_layout.addWidget(sep_1, 0) + main_layout.addWidget(detail_widget, 1) + main_layout.addWidget(sep_2, 0) + main_layout.addWidget(btns_widget, 0) + + copy_btn.clicked.connect(self._on_copy_click) + close_btn.clicked.connect(self._on_close_click) + + self._title_label = title_label + self._detail_widget = detail_widget + + def set_detail(self, title, detail): + self._title_label.setText(title) + self._detail_widget.setText(detail) + + def _on_copy_click(self): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(self._detail_widget.toPlainText()) + + def _on_close_click(self): + self.close() + + class PushToContextSelectWindow(QtWidgets.QWidget): def __init__(self, controller=None): super(PushToContextSelectWindow, self).__init__() @@ -66,9 +123,12 @@ def __init__(self, controller=None): # --- Inputs widget --- inputs_widget = QtWidgets.QWidget(main_splitter) + new_folder_checkbox = NiceCheckbox(True, parent=inputs_widget) + folder_name_input = PlaceholderLineEdit(inputs_widget) folder_name_input.setPlaceholderText("< Name of new folder >") folder_name_input.setObjectName("ValidatedLineEdit") + folder_name_input.setEnabled(new_folder_checkbox.isChecked()) variant_input = PlaceholderLineEdit(inputs_widget) variant_input.setPlaceholderText("< Variant >") @@ -79,6 +139,7 @@ def __init__(self, controller=None): inputs_layout = QtWidgets.QFormLayout(inputs_widget) inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.addRow("Create new folder", new_folder_checkbox) inputs_layout.addRow("New folder name", folder_name_input) inputs_layout.addRow("Variant", variant_input) inputs_layout.addRow("Comment", comment_input) @@ -113,6 +174,10 @@ def __init__(self, controller=None): overlay_label = QtWidgets.QLabel(overlay_widget) overlay_label.setAlignment(QtCore.Qt.AlignCenter) + overlay_label.setWordWrap(True) + overlay_label.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) overlay_btns_widget = QtWidgets.QWidget(overlay_widget) overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) @@ -121,13 +186,28 @@ def __init__(self, controller=None): overlay_try_btn = QtWidgets.QPushButton( "Try again", overlay_btns_widget ) + overlay_try_btn.setToolTip( + "Hide overlay and modify submit information." + ) + + show_detail_btn = QtWidgets.QPushButton( + "Show error detail", overlay_btns_widget + ) + show_detail_btn.setToolTip( + "Show error detail dialog to copy full error." + ) + overlay_close_btn = QtWidgets.QPushButton( "Close", overlay_btns_widget ) + overlay_close_btn.setToolTip("Discard changes and close window.") overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget) + overlay_btns_layout.setContentsMargins(0, 0, 0, 0) + overlay_btns_layout.setSpacing(10) overlay_btns_layout.addStretch(1) overlay_btns_layout.addWidget(overlay_try_btn, 0) + overlay_btns_layout.addWidget(show_detail_btn, 0) overlay_btns_layout.addWidget(overlay_close_btn, 0) overlay_btns_layout.addStretch(1) @@ -156,12 +236,14 @@ def __init__(self, controller=None): main_thread_timer.timeout.connect(self._on_main_thread_timer) show_timer.timeout.connect(self._on_show_timer) user_input_changed_timer.timeout.connect(self._on_user_input_timer) + new_folder_checkbox.stateChanged.connect(self._on_new_folder_check) folder_name_input.textChanged.connect(self._on_new_folder_change) variant_input.textChanged.connect(self._on_variant_change) comment_input.textChanged.connect(self._on_comment_change) publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) + show_detail_btn.clicked.connect(self._on_show_detail_click) overlay_close_btn.clicked.connect(self._on_close_click) overlay_try_btn.clicked.connect(self._on_try_again_click) @@ -203,23 +285,28 @@ def __init__(self, controller=None): self._tasks_widget = tasks_widget self._variant_input = variant_input + self._new_folder_checkbox = new_folder_checkbox self._folder_name_input = folder_name_input self._comment_input = comment_input self._publish_btn = publish_btn self._overlay_widget = overlay_widget + self._show_detail_btn = show_detail_btn self._overlay_close_btn = overlay_close_btn self._overlay_try_btn = overlay_try_btn self._overlay_label = overlay_label + self._error_detail_dialog = ErrorDetailDialog(self) + self._user_input_changed_timer = user_input_changed_timer # Store current value on input text change # The value is unset when is passed to controller # The goal is to have controll over changes happened during user change # in UI and controller auto-changes - self._variant_input_text = None + self._new_folder_name_enabled = None self._new_folder_name_input_text = None + self._variant_input_text = None self._comment_input_text = None self._first_show = True @@ -235,6 +322,7 @@ def __init__(self, controller=None): self._folder_is_valid = None publish_btn.setEnabled(False) + show_detail_btn.setVisible(False) overlay_close_btn.setVisible(False) overlay_try_btn.setVisible(False) @@ -289,6 +377,11 @@ def _on_show_timer(self): self.refresh() + def _on_new_folder_check(self): + self._new_folder_name_enabled = self._new_folder_checkbox.isChecked() + self._folder_name_input.setEnabled(self._new_folder_name_enabled) + self._user_input_changed_timer.start() + def _on_new_folder_change(self, text): self._new_folder_name_input_text = text self._user_input_changed_timer.start() @@ -302,9 +395,15 @@ def _on_comment_change(self, text): self._user_input_changed_timer.start() def _on_user_input_timer(self): + folder_name_enabled = self._new_folder_name_enabled folder_name = self._new_folder_name_input_text - if folder_name is not None: + if folder_name is not None or folder_name_enabled is not None: self._new_folder_name_input_text = None + self._new_folder_name_enabled = None + if not self._new_folder_checkbox.isChecked(): + folder_name = None + elif folder_name is None: + folder_name = self._folder_name_input.text() self._controller.set_user_value_folder_name(folder_name) variant = self._variant_input_text @@ -350,16 +449,13 @@ def _on_controller_source_change(self): self._header_label.setText(self._controller.get_source_label()) def _invalidate_new_folder_name(self, folder_name, is_valid): - self._tasks_widget.setVisible(not folder_name) + self._tasks_widget.setVisible(folder_name is None) if self._folder_is_valid is is_valid: return self._folder_is_valid = is_valid state = "" - if folder_name: - if is_valid is True: - state = "valid" - elif is_valid is False: - state = "invalid" + if folder_name is not None: + state = "valid" if is_valid else "invalid" set_style_property( self._folder_name_input, "state", state ) @@ -374,6 +470,9 @@ def _invalidate_variant(self, is_valid): def _on_submission_change(self, event): self._publish_btn.setEnabled(event["enabled"]) + def _on_show_detail_click(self): + self._error_detail_dialog.show() + def _on_close_click(self): self.close() @@ -384,8 +483,11 @@ def _on_try_again_click(self): self._process_item_id = None self._last_submit_message = None + self._error_detail_dialog.close() + self._overlay_close_btn.setVisible(False) self._overlay_try_btn.setVisible(False) + self._show_detail_btn.setVisible(False) self._main_layout.setCurrentWidget(self._main_context_widget) def _on_main_thread_timer(self): @@ -401,13 +503,24 @@ def _on_main_thread_timer(self): if self._main_thread_timer_can_stop: self._main_thread_timer.stop() self._overlay_close_btn.setVisible(True) - if push_failed and not fail_traceback: + if push_failed: self._overlay_try_btn.setVisible(True) + if fail_traceback: + self._show_detail_btn.setVisible(True) if push_failed: - message = "Push Failed:\n{}".format(process_status["fail_reason"]) + reason = process_status["fail_reason"] if fail_traceback: - message += "\n{}".format(fail_traceback) + message = ( + "Unhandled error happened." + " Check error detail for more information." + ) + self._error_detail_dialog.set_detail( + reason, fail_traceback + ) + else: + message = f"Push Failed:\n{reason}" + self._overlay_label.setText(message) set_style_property(self._overlay_close_btn, "state", "error")