diff --git a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml index 2de0e78cc7..0f8544285f 100644 --- a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml +++ b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml @@ -18,7 +18,7 @@ Item width: parent.width height: parent.height - property alias createNewProjectButtonVisible: createNewProjectButton.visible + property bool createNewProjectButtonVisible: true anchors { @@ -48,6 +48,7 @@ Item anchors.verticalCenter: selectProjectLabel.verticalCenter anchors.right: parent.right text: "New Library project" + visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed) onClicked: { @@ -56,6 +57,20 @@ Item busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress } + + Cura.SecondaryButton + { + id: upgradePlanButton + + anchors.verticalCenter: selectProjectLabel.verticalCenter + anchors.right: parent.right + text: "Upgrade plan" + visible: createNewProjectButtonVisible && !manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed) + tooltip: "You have reached the maximum number of projects allowed by your subscription. Please upgrade to the Professional subscription to create more projects." + + onClicked: Qt.openUrlExternally("https://ultimaker.com/software/enterprise-software") + } + Item { id: noLibraryProjectsContainer diff --git a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py index e1a62fdd5c..acb7b9a5a5 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py @@ -55,6 +55,7 @@ class DigitalFactoryApiClient: self._http = HttpRequestManager.getInstance() self._on_error = on_error self._file_uploader = None # type: Optional[DFFileUploader] + self._library_max_private_projects: Optional[int] = None self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager] @@ -69,6 +70,7 @@ class DigitalFactoryApiClient: callback( response.library_max_private_projects == -1 or # Note: -1 is unlimited response.library_max_private_projects > 0) + self._library_max_private_projects = response.library_max_private_projects else: Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}") callback(False) @@ -79,6 +81,27 @@ class DigitalFactoryApiClient: error_callback = callbackWrap, timeout = self.DEFAULT_REQUEST_TIMEOUT) + def checkUserCanCreateNewLibraryProject(self, callback: Callable) -> None: + """ + Checks if the user is allowed to create new library projects. + A user is allowed to create new library projects if the haven't reached their maximum allowed private projects. + """ + + def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None: + if response is not None: + if self._library_max_private_projects == -1 or isinstance(response, DigitalFactoryProjectResponse): + callback(True) + elif isinstance(response, list) and all(isinstance(r, DigitalFactoryProjectResponse) for r in response): + callback(len(response) < self._library_max_private_projects) + else: + Logger.warning(f"Digital Factory: Incorrect response type received when requesting private projects: {str(response)}") + callback(False) + else: + Logger.warning(f"Digital Factory: Response is empty, likely an error: {str(response)}") + callback(False) + + self.getProjectsFirstPage(on_finished = callbackWrap, failed = callbackWrap) + def getProject(self, library_project_id: str, on_finished: Callable[[DigitalFactoryProjectResponse], Any], failed: Callable) -> None: """ Retrieves a digital factory project by its library project id. diff --git a/plugins/DigitalLibrary/src/DigitalFactoryController.py b/plugins/DigitalLibrary/src/DigitalFactoryController.py index 368b29219a..cf4174d7c9 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryController.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryController.py @@ -92,6 +92,9 @@ class DigitalFactoryController(QObject): """Signal to inform about the state of user access.""" userAccessStateChanged = pyqtSignal(bool) + """Signal to inform whether the user is allowed to create more Library projects.""" + userCanCreateNewLibraryProjectChanged = pyqtSignal(bool) + def __init__(self, application: CuraApplication) -> None: super().__init__(parent = None) @@ -136,6 +139,7 @@ class DigitalFactoryController(QObject): self._application.initializationFinished.connect(self._applicationInitializationFinished) self._user_has_access = False + self._user_account_can_create_new_project = False def clear(self) -> None: self._project_model.clearProjects() @@ -166,6 +170,11 @@ class DigitalFactoryController(QObject): subscriptions = self._account.userProfile.get("subscriptions", []) if len(subscriptions) > 0: return True + if self._user_has_access: + # The user has access even though they have no subscriptions. This means they are an Essential user and they + # have limited personal private projects available. In this case, we need to check whether they have already + # reached their limit. + self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject) return self._user_has_access def initialize(self, preselected_project_id: Optional[str] = None) -> None: @@ -517,6 +526,7 @@ class DigitalFactoryController(QObject): self._project_model.clearProjects() self.setSelectedProjectIndex(-1) self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed) + self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject) self.setRetrievingProjectsStatus(RetrievalStatus.InProgress) self._has_preselected_project = new_has_preselected_project self.preselectedProjectChanged.emit() @@ -525,6 +535,14 @@ class DigitalFactoryController(QObject): def hasPreselectedProject(self) -> bool: return self._has_preselected_project + def setCanCreateNewLibraryProject(self, can_create_new_library_project: bool) -> None: + self._user_account_can_create_new_project = can_create_new_library_project + self.userCanCreateNewLibraryProjectChanged.emit(self._user_account_can_create_new_project) + + @pyqtProperty(bool, fset = setCanCreateNewLibraryProject, notify = userCanCreateNewLibraryProjectChanged) + def userAccountCanCreateNewLibraryProject(self) -> bool: + return self._user_account_can_create_new_project + @pyqtSlot(str, "QStringList") def saveFileToSelectedProject(self, filename: str, formats: List[str]) -> None: """