From fb2704ee638c198bfc4ae0e2c74eba3800dcaf5d Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Tue, 29 Oct 2019 17:24:07 +0200 Subject: [PATCH] Disable autosave on Devfile editor page (#14913) * Remove autosave on Devfile editor page Signed-off-by: Oleksii Kurinnyi * update ConfirmDialogService Dialog window can have now only one button. Signed-off-by: Oleksii Kurinnyi * fix applying changes to running workspace Signed-off-by: Oleksii Kurinnyi --- .../user-management.controller.ts | 4 +- .../docker-registry-list.controller.ts | 2 +- .../factory-information.controller.ts | 4 +- .../list-factories.controller.ts | 2 +- .../recent-workspaces.controller.ts | 66 ++- .../list-organizations.controller.ts | 2 +- .../organizations-item.controller.ts | 2 +- .../organization-details.controller.ts | 2 +- .../list-organization-members.controller.ts | 4 +- src/app/teams/list/list-teams.controller.ts | 2 +- .../list/team-item/team-item.controller.ts | 2 +- .../team-details/team-details.controller.ts | 4 +- .../list-team-members.controller.ts | 2 +- .../member-item/member-item.controller.ts | 2 +- .../create-workspace.service.ts | 10 +- .../list-workspaces.controller.ts | 2 +- .../share-workspace.controller.ts | 2 +- .../user-item/user-item.controller.ts | 2 +- .../workspace-devfile-editor.controller.ts | 102 ++--- .../list-env-variables.controller.ts | 2 +- .../list-servers/list-servers.controller.ts | 2 +- .../machine-config.controller.ts | 2 +- .../list-commands/list-commands.controller.ts | 2 +- .../workspace-details.controller.ts | 397 ++++++++---------- .../workspace-details.directive.spec.ts | 133 ++++-- .../workspace-details/workspace-details.html | 56 +-- .../workspace-details.service.ts | 137 ++++-- .../env-variables.controller.ts | 4 +- .../machine-servers.controller.ts | 4 +- .../machine-volumes.controller.ts | 4 +- .../workspace-machines.controller.ts | 4 +- .../workspace-details-overview.controller.ts | 2 +- .../workspace-details-overview.html | 2 +- .../workspace-details-projects.controller.ts | 2 +- .../confirm-dialog/che-confirm-dialog.html | 5 +- .../confirm-dialog/confirm-dialog.service.ts | 9 +- .../che-edit-mode-overlay.controller.ts | 78 ++++ .../che-edit-mode-overlay.directive.ts | 80 +++- src/components/widget/widget-config.ts | 2 + 39 files changed, 707 insertions(+), 438 deletions(-) create mode 100644 src/components/widget/edit-mode-overlay/che-edit-mode-overlay.controller.ts diff --git a/src/app/admin/user-management/user-management.controller.ts b/src/app/admin/user-management/user-management.controller.ts index 7e890d7e83f..c36ab472744 100644 --- a/src/app/admin/user-management/user-management.controller.ts +++ b/src/app/admin/user-management/user-management.controller.ts @@ -144,7 +144,7 @@ export class AdminsUserManagementCtrl { */ removeUser(event: MouseEvent, user: any): void { let content = 'Are you sure you want to remove \'' + user.email + '\'?'; - let promise = this.confirmDialogService.showConfirmDialog('Remove user', content, 'Delete', 'Cancel'); + let promise = this.confirmDialogService.showConfirmDialog('Remove user', content, { resolve: 'Delete', reject: 'Cancel' }); promise.then(() => { this.isLoading = true; @@ -231,7 +231,7 @@ export class AdminsUserManagementCtrl { content += 'user?'; } - return this.confirmDialogService.showConfirmDialog('Remove users', content, 'Delete', 'Cancel'); + return this.confirmDialogService.showConfirmDialog('Remove users', content, { resolve: 'Delete', reject: 'Cancel' }); } /** diff --git a/src/app/administration/docker-registry/docker-registry-list/docker-registry-list.controller.ts b/src/app/administration/docker-registry/docker-registry-list/docker-registry-list.controller.ts index 4b5ce8b87f1..1d12298ddbd 100644 --- a/src/app/administration/docker-registry/docker-registry-list/docker-registry-list.controller.ts +++ b/src/app/administration/docker-registry/docker-registry-list/docker-registry-list.controller.ts @@ -163,7 +163,7 @@ export class DockerRegistryListController { } else { content += 'this selected registry?'; } - return this.confirmDialogService.showConfirmDialog('Remove registries', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove registries', content, { resolve: 'Delete' }); } /** diff --git a/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts b/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts index 11bb04ef576..aa5f64dfc3f 100644 --- a/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts +++ b/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts @@ -218,7 +218,7 @@ export class FactoryInformationController { const title = 'Warning', content = `You have unsaved changes in JSON configuration. Would you like to save changes now?`; - return this.confirmDialogService.showConfirmDialog(title, content, 'Continue').then(() => { + return this.confirmDialogService.showConfirmDialog(title, content, { resolve: 'Continue' }).then(() => { this.updateFactoryContent(); }); } @@ -296,7 +296,7 @@ export class FactoryInformationController { */ deleteFactory(): void { let content = 'Please confirm removal for the factory \'' + (this.factory.name ? this.factory.name : this.factory.id) + '\'.'; - let promise = this.confirmDialogService.showConfirmDialog('Remove the factory', content, 'Delete'); + let promise = this.confirmDialogService.showConfirmDialog('Remove the factory', content, { resolve: 'Delete' }); promise.then(() => { // remove it ! diff --git a/src/app/factories/list-factories/list-factories.controller.ts b/src/app/factories/list-factories/list-factories.controller.ts index 94e54389114..bd46e06a8d0 100644 --- a/src/app/factories/list-factories/list-factories.controller.ts +++ b/src/app/factories/list-factories/list-factories.controller.ts @@ -227,6 +227,6 @@ export class ListFactoriesController { } else { content += 'this selected factory?'; } - return this.confirmDialogService.showConfirmDialog('Remove factories', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove factories', content, { resolve: 'Delete' }); } } diff --git a/src/app/navbar/recent-workspaces/recent-workspaces.controller.ts b/src/app/navbar/recent-workspaces/recent-workspaces.controller.ts index f62468a381a..b74119717e4 100644 --- a/src/app/navbar/recent-workspaces/recent-workspaces.controller.ts +++ b/src/app/navbar/recent-workspaces/recent-workspaces.controller.ts @@ -10,11 +10,12 @@ * Red Hat, Inc. - initial API and implementation */ 'use strict'; -import {CheWorkspace} from '../../../components/api/workspace/che-workspace.factory'; +import { CheWorkspace } from '../../../components/api/workspace/che-workspace.factory'; import IdeSvc from '../../../app/ide/ide.service'; -import {CheBranding} from '../../../components/branding/che-branding.factory'; -import {WorkspacesService} from '../../workspaces/workspaces.service'; -import {CheNotification} from '../../../components/notification/che-notification.factory'; +import { CheBranding } from '../../../components/branding/che-branding.factory'; +import { WorkspacesService } from '../../workspaces/workspaces.service'; +import { CheNotification } from '../../../components/notification/che-notification.factory'; +import { WorkspaceDetailsService } from '../../workspaces/workspace-details/workspace-details.service'; const MAX_RECENT_WORKSPACES_ITEMS: number = 5; @@ -27,7 +28,18 @@ const MAX_RECENT_WORKSPACES_ITEMS: number = 5; */ export class NavbarRecentWorkspacesController { - static $inject = ['ideSvc', 'cheWorkspace', 'cheBranding', '$window', '$log', '$scope', '$rootScope', 'workspacesService', 'cheNotification']; + static $inject = [ + 'ideSvc', + 'cheWorkspace', + 'cheBranding', + '$window', + '$log', + '$scope', + '$rootScope', + 'workspacesService', + 'cheNotification', + 'workspaceDetailsService' + ]; cheWorkspace: CheWorkspace; dropdownItemTempl: Array; @@ -45,19 +57,23 @@ export class NavbarRecentWorkspacesController { workspacesService: WorkspacesService; cheNotification: CheNotification; cheBranding: CheBranding; + workspaceDetailsService: WorkspaceDetailsService; /** * Default constructor */ - constructor(ideSvc: IdeSvc, - cheWorkspace: CheWorkspace, - cheBranding: CheBranding, - $window: ng.IWindowService, - $log: ng.ILogService, - $scope: ng.IScope, - $rootScope: ng.IRootScopeService, - workspacesService: WorkspacesService, - cheNotification: CheNotification) { + constructor( + ideSvc: IdeSvc, + cheWorkspace: CheWorkspace, + cheBranding: CheBranding, + $window: ng.IWindowService, + $log: ng.ILogService, + $scope: ng.IScope, + $rootScope: ng.IRootScopeService, + workspacesService: WorkspacesService, + cheNotification: CheNotification, + workspaceDetailsService: WorkspaceDetailsService + ) { this.ideSvc = ideSvc; this.cheWorkspace = cheWorkspace; this.$log = $log; @@ -66,6 +82,7 @@ export class NavbarRecentWorkspacesController { this.workspacesService = workspacesService; this.cheNotification = cheNotification; this.cheBranding = cheBranding; + this.workspaceDetailsService = workspaceDetailsService; // workspace updated time map by id this.workspaceUpdated = new Map(); @@ -320,6 +337,11 @@ export class NavbarRecentWorkspacesController { * @param workspaceId {String} workspace id */ stopRecentWorkspace(workspaceId: string): void { + if (this.checkUnsavedChanges(workspaceId)) { + this.workspaceDetailsService.notifyUnsavedChangesDialog(); + return; + } + this.cheWorkspace.stopWorkspace(workspaceId).then(() => { angular.noop(); }, (error: any) => { @@ -333,11 +355,16 @@ export class NavbarRecentWorkspacesController { * @param workspaceId {String} workspace id */ runRecentWorkspace(workspaceId: string): void { + if (this.checkUnsavedChanges(workspaceId)) { + this.workspaceDetailsService.notifyUnsavedChangesDialog(); + return; + } + let workspace = this.cheWorkspace.getWorkspaceById(workspaceId); this.updateRecentWorkspace(workspaceId); - this.cheWorkspace.startWorkspace(workspace.id, workspace.config ? workspace.config.defaultEnv: null).catch((error: any) => { + this.cheWorkspace.startWorkspace(workspace.id, workspace.config ? workspace.config.defaultEnv : null).catch((error: any) => { this.$log.error(error); this.cheNotification.showError('Run workspace error.', error); }); @@ -352,4 +379,13 @@ export class NavbarRecentWorkspacesController { updateRecentWorkspace(workspaceId: string): void { this.$rootScope.$broadcast('recent-workspace:set', workspaceId); } + + /** + * Returns `true` if workspace configuration has unsaved changes. + * @param id a workspace ID + */ + checkUnsavedChanges(id: string): ng.IPromise | any { + return this.workspaceDetailsService.isWorkspaceConfigSaved(id) === false; + } + } diff --git a/src/app/organizations/list-organizations/list-organizations.controller.ts b/src/app/organizations/list-organizations/list-organizations.controller.ts index 6c68604e516..d94e72b8a0d 100644 --- a/src/app/organizations/list-organizations/list-organizations.controller.ts +++ b/src/app/organizations/list-organizations/list-organizations.controller.ts @@ -338,7 +338,7 @@ export class ListOrganizationsController { content += 'this selected organization?'; } - return this.confirmDialogService.showConfirmDialog('Delete organizations', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Delete organizations', content, { resolve: 'Delete' }); } } diff --git a/src/app/organizations/list-organizations/organizations-item/organizations-item.controller.ts b/src/app/organizations/list-organizations/organizations-item/organizations-item.controller.ts index f64ec5f93fb..b395b1cb864 100644 --- a/src/app/organizations/list-organizations/organizations-item/organizations-item.controller.ts +++ b/src/app/organizations/list-organizations/organizations-item/organizations-item.controller.ts @@ -140,6 +140,6 @@ export class OrganizationsItemController { */ confirmRemoval(): ng.IPromise { return this.confirmDialogService.showConfirmDialog('Delete organization', - 'Would you like to delete organization \'' + this.organization.name + '\'?', 'Delete'); + 'Would you like to delete organization \'' + this.organization.name + '\'?', { resolve: 'Delete' }); } } diff --git a/src/app/organizations/organization-details/organization-details.controller.ts b/src/app/organizations/organization-details/organization-details.controller.ts index 29ec249ea71..a490687864b 100644 --- a/src/app/organizations/organization-details/organization-details.controller.ts +++ b/src/app/organizations/organization-details/organization-details.controller.ts @@ -406,7 +406,7 @@ export class OrganizationDetailsController { */ deleteOrganization(): void { let promise = this.confirmDialogService.showConfirmDialog('Delete organization', - 'Would you like to delete organization \'' + this.organization.name + '\'?', 'Delete'); + 'Would you like to delete organization \'' + this.organization.name + '\'?', { resolve: 'Delete' }); promise.then(() => { let promise = this.cheOrganization.deleteOrganization(this.organization.id); diff --git a/src/app/organizations/organization-details/organization-members/list-organization-members.controller.ts b/src/app/organizations/organization-details/organization-members/list-organization-members.controller.ts index 8f801ddf8c5..707fdc22913 100644 --- a/src/app/organizations/organization-details/organization-members/list-organization-members.controller.ts +++ b/src/app/organizations/organization-details/organization-members/list-organization-members.controller.ts @@ -404,7 +404,7 @@ export class ListOrganizationMembersController { * @param member */ removeMember(member: che.IMember): void { - let promise = this.confirmDialogService.showConfirmDialog('Remove member', 'Would you like to remove member ' + member.email + ' ?', 'Delete'); + let promise = this.confirmDialogService.showConfirmDialog('Remove member', 'Would you like to remove member ' + member.email + ' ?', { resolve: 'Delete' }); promise.then(() => { this.removePermissions(member); @@ -507,6 +507,6 @@ export class ListOrganizationMembersController { confirmTitle += 'the selected member?'; } - return this.confirmDialogService.showConfirmDialog('Remove members', confirmTitle, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove members', confirmTitle, { resolve: 'Delete' }); } } diff --git a/src/app/teams/list/list-teams.controller.ts b/src/app/teams/list/list-teams.controller.ts index dbf2599fc5d..12a65545bf7 100644 --- a/src/app/teams/list/list-teams.controller.ts +++ b/src/app/teams/list/list-teams.controller.ts @@ -359,6 +359,6 @@ export class ListTeamsController { content += 'this selected team?'; } - return this.confirmDialogService.showConfirmDialog('Delete teams', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Delete teams', content, { resolve: 'Delete' }); } } diff --git a/src/app/teams/list/team-item/team-item.controller.ts b/src/app/teams/list/team-item/team-item.controller.ts index 3265be0101e..7ff9460ed6a 100644 --- a/src/app/teams/list/team-item/team-item.controller.ts +++ b/src/app/teams/list/team-item/team-item.controller.ts @@ -94,7 +94,7 @@ export class TeamItemController { */ confirmRemoval(): ng.IPromise { let promise = this.confirmDialogService.showConfirmDialog('Delete team', - 'Would you like to delete team \'' + this.team.name + '\'?', 'Delete'); + 'Would you like to delete team \'' + this.team.name + '\'?', { resolve: 'Delete' }); return promise; } } diff --git a/src/app/teams/team-details/team-details.controller.ts b/src/app/teams/team-details/team-details.controller.ts index 529f20e987e..90573ead655 100644 --- a/src/app/teams/team-details/team-details.controller.ts +++ b/src/app/teams/team-details/team-details.controller.ts @@ -315,7 +315,7 @@ tab: Object = Tab; */ deleteTeam(event: MouseEvent): void { let promise = this.confirmDialogService.showConfirmDialog('Delete team', - 'Would you like to delete team \'' + this.team.name + '\'?', 'Delete'); + 'Would you like to delete team \'' + this.team.name + '\'?', { resolve: 'Delete' }); promise.then(() => { let promise = this.cheTeam.deleteTeam(this.team.id); @@ -334,7 +334,7 @@ tab: Object = Tab; */ leaveTeam(): void { let promise = this.confirmDialogService.showConfirmDialog('Leave team', - 'Would you like to leave team \'' + this.team.name + '\'?', 'Leave'); + 'Would you like to leave team \'' + this.team.name + '\'?', { resolve: 'Leave' }); promise.then(() => { let promise = this.chePermissions.removeOrganizationPermissions(this.team.id, this.cheUser.getUser().id); diff --git a/src/app/teams/team-details/team-members/list-team-members.controller.ts b/src/app/teams/team-details/team-members/list-team-members.controller.ts index 0a083209f79..09841309d32 100644 --- a/src/app/teams/team-details/team-members/list-team-members.controller.ts +++ b/src/app/teams/team-details/team-members/list-team-members.controller.ts @@ -554,6 +554,6 @@ export class ListTeamMembersController { confirmTitle += 'the selected member?'; } - return this.confirmDialogService.showConfirmDialog('Remove members', confirmTitle, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove members', confirmTitle, { resolve: 'Delete' }); } } diff --git a/src/app/teams/team-details/team-members/member-item/member-item.controller.ts b/src/app/teams/team-details/team-members/member-item/member-item.controller.ts index a26d839394e..fe0ce50aebc 100644 --- a/src/app/teams/team-details/team-members/member-item/member-item.controller.ts +++ b/src/app/teams/team-details/team-members/member-item/member-item.controller.ts @@ -73,7 +73,7 @@ export class MemberItemController { * @param event - the $event */ removeMember(event: MouseEvent): void { - let promise = this.confirmDialogService.showConfirmDialog('Remove member', 'Would you like to remove member ' + this.member.email + ' ?', 'Delete'); + let promise = this.confirmDialogService.showConfirmDialog('Remove member', 'Would you like to remove member ' + this.member.email + ' ?', { resolve: 'Delete' }); promise.then(() => { if (this.member.isPending) { diff --git a/src/app/workspaces/create-workspace/create-workspace.service.ts b/src/app/workspaces/create-workspace/create-workspace.service.ts index 8eedc46b6b2..0ae9c14d286 100644 --- a/src/app/workspaces/create-workspace/create-workspace.service.ts +++ b/src/app/workspaces/create-workspace/create-workspace.service.ts @@ -197,8 +197,8 @@ export class CreateWorkspaceSvc { }); projects.push(template); - }); - + }); + return this.checkEditingProgress().then(() => { sourceDevfile.projects = projects; @@ -206,7 +206,7 @@ export class CreateWorkspaceSvc { if (noProjectsFromDevfile) { sourceDevfile.commands = []; } - + return this.cheWorkspace.createWorkspaceFromDevfile(namespaceId, sourceDevfile, attributes).then((workspace: che.IWorkspace) => { return this.cheWorkspace.fetchWorkspaces().then(() => this.cheWorkspace.getWorkspaceById(workspace.id)); }) @@ -246,7 +246,7 @@ export class CreateWorkspaceSvc { const title = 'Warning', content = `You have project editing, that is not completed. Would you like to proceed to workspace creation without these changes?`; - return this.confirmDialogService.showConfirmDialog(title, content, 'Continue'); + return this.confirmDialogService.showConfirmDialog(title, content, { resolve: 'Continue' }); } /** @@ -289,7 +289,7 @@ export class CreateWorkspaceSvc { /** * Returns name of the pointed workspace. - * + * * @param workspace workspace */ getWorkspaceName(workspace: che.IWorkspace): string { diff --git a/src/app/workspaces/list-workspaces/list-workspaces.controller.ts b/src/app/workspaces/list-workspaces/list-workspaces.controller.ts index 432bb9ba4f1..7b2ed207818 100644 --- a/src/app/workspaces/list-workspaces/list-workspaces.controller.ts +++ b/src/app/workspaces/list-workspaces/list-workspaces.controller.ts @@ -305,7 +305,7 @@ export class ListWorkspacesCtrl { content += 'this selected workspace?'; } - return this.confirmDialogService.showConfirmDialog('Remove workspaces', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove workspaces', content, { resolve: 'Delete' }); } /** diff --git a/src/app/workspaces/share-workspace/share-workspace.controller.ts b/src/app/workspaces/share-workspace/share-workspace.controller.ts index 7297a4ce12d..742bccfab03 100644 --- a/src/app/workspaces/share-workspace/share-workspace.controller.ts +++ b/src/app/workspaces/share-workspace/share-workspace.controller.ts @@ -344,7 +344,7 @@ export class ShareWorkspaceController { content += 'this selected member?'; } - return this.confirmDialogService.showConfirmDialog('Remove members', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove members', content, { resolve: 'Delete' }); } /** diff --git a/src/app/workspaces/share-workspace/user-item/user-item.controller.ts b/src/app/workspaces/share-workspace/user-item/user-item.controller.ts index 8d74e4a8345..bf7cc79ddd7 100644 --- a/src/app/workspaces/share-workspace/user-item/user-item.controller.ts +++ b/src/app/workspaces/share-workspace/user-item/user-item.controller.ts @@ -43,7 +43,7 @@ export class UserItemController { */ removeUser(): void { let content = 'Please confirm removal for the member \'' + this.user.email + '\'.'; - let promise = this.confirmDialogService.showConfirmDialog('Remove the member', content, 'Delete'); + let promise = this.confirmDialogService.showConfirmDialog('Remove the member', content, { resolve: 'Delete' }); promise.then(() => { // callback is set in scope definition: diff --git a/src/app/workspaces/workspace-details/devfile/workspace-devfile-editor.controller.ts b/src/app/workspaces/workspace-details/devfile/workspace-devfile-editor.controller.ts index 81b922ca46a..8f8ddd1e68d 100644 --- a/src/app/workspaces/workspace-details/devfile/workspace-devfile-editor.controller.ts +++ b/src/app/workspaces/workspace-details/devfile/workspace-devfile-editor.controller.ts @@ -18,28 +18,29 @@ */ export class WorkspaceDevfileEditorController { - static $inject = ['$log', '$scope', '$timeout']; + static $inject = [ + '$log', + '$scope', + '$timeout' + ]; + private $log: ng.ILogService; + private $scope: ng.IScope; + private $timeout: ng.ITimeoutService; - $log: ng.ILogService; - $scope: ng.IScope; - $timeout: ng.ITimeoutService; + private isActive: boolean; + private workspaceDevfile: che.IWorkspaceDevfile; + private workspaceDevfileOnChange: Function; - editorOptions: { + private editorOptions: { lineWrapping: boolean, lineNumbers: boolean, matchBrackets: boolean, mode: string, onLoad: Function }; - devfileValidationMessages: string[] = []; - isActive: boolean; - workspaceDevfile: che.IWorkspaceDevfile; - devfileYaml: string; - newWorkspaceDevfile: che.IWorkspaceDevfile; - workspaceDevfileOnChange: Function; + private validationErrors: string[] = []; + private devfileYaml: string; private saveTimeoutPromise: ng.IPromise; - private isSaving: boolean; - /** * Default constructor that is using resource @@ -48,70 +49,73 @@ export class WorkspaceDevfileEditorController { this.$log = $log; this.$scope = $scope; this.$timeout = $timeout; - this.isSaving = false; - this.devfileYaml = jsyaml.dump(this.workspaceDevfile); + + this.$scope.$on('edit-workspace-details', (event: ng.IAngularEvent, attrs: { status: string }) => { + if (attrs.status === 'cancelled') { + this.$onInit(); + } + }); $scope.$watch(() => { return this.workspaceDevfile; }, () => { - let editedWorkspaceDevfile; + let devfile: che.IWorkspaceDevfile; try { - editedWorkspaceDevfile = jsyaml.load(this.devfileYaml); - angular.extend(editedWorkspaceDevfile, this.workspaceDevfile); + devfile = jsyaml.safeLoad(this.devfileYaml); } catch (e) { - editedWorkspaceDevfile = this.workspaceDevfile; + return; + } + + if (angular.equals(devfile, this.workspaceDevfile) === false) { + angular.extend(devfile, this.workspaceDevfile); + this.devfileYaml = jsyaml.safeDump(devfile); + this.validate(); } - this.devfileYaml = jsyaml.dump(this.workspaceDevfile); - const validateOnly = true; - this.onChange(validateOnly); }, true); } - $onInit(): void { } + $onInit(): void { + this.devfileYaml = jsyaml.safeDump(this.workspaceDevfile); + } - /** - * Callback when editor content is changed. - */ - onChange(validateOnly?: boolean): void { - this.devfileValidationMessages = []; - if (!this.devfileYaml) { - return; - } + validate() { + this.validationErrors = []; - let devfile; + let devfile: che.IWorkspaceDevfile; try { - devfile = jsyaml.load(this.devfileYaml); + devfile = jsyaml.safeLoad(this.devfileYaml); } catch (e) { if (e.name === 'YAMLException') { - this.devfileValidationMessages = [e.message]; + this.validationErrors = [e.message]; } - if (this.devfileValidationMessages.length === 0) { - this.devfileValidationMessages = ['Devfile is invalid.']; + if (this.validationErrors.length === 0) { + this.validationErrors = ['Devfile is invalid.']; } this.$log.error(e); } + } - if (validateOnly || !this.isActive) { + /** + * Callback when editor content is changed. + */ + onChange(): void { + if (!this.isActive) { return; } - this.isSaving = (this.devfileValidationMessages.length === 0) && !angular.equals(devfile, this.workspaceDevfile); if (this.saveTimeoutPromise) { this.$timeout.cancel(this.saveTimeoutPromise); } this.saveTimeoutPromise = this.$timeout(() => { - // immediately apply config on IU - this.newWorkspaceDevfile = angular.copy(devfile); - this.isSaving = false; - this.applyChanges(); - }, 2000); - } + this.validate(); + if (this.validationErrors.length !== 0) { + return; + } - /** - * Callback when user applies new config. - */ - applyChanges(): void { - this.workspaceDevfileOnChange({devfile: this.newWorkspaceDevfile}); + angular.extend(this.workspaceDevfile, jsyaml.safeLoad(this.devfileYaml)); + this.workspaceDevfileOnChange(); + }, 200); } + } diff --git a/src/app/workspaces/workspace-details/environments/list-env-variables/list-env-variables.controller.ts b/src/app/workspaces/workspace-details/environments/list-env-variables/list-env-variables.controller.ts index 65fb6622231..6ed3b562194 100644 --- a/src/app/workspaces/workspace-details/environments/list-env-variables/list-env-variables.controller.ts +++ b/src/app/workspaces/workspace-details/environments/list-env-variables/list-env-variables.controller.ts @@ -191,7 +191,7 @@ export class ListEnvVariablesController { content += 'this selected variable?'; } - return this.confirmDialogService.showConfirmDialog('Remove variables', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove variables', content, { resolve: 'Delete' }); } } diff --git a/src/app/workspaces/workspace-details/environments/list-servers/list-servers.controller.ts b/src/app/workspaces/workspace-details/environments/list-servers/list-servers.controller.ts index f808a006550..6ba25e405ce 100644 --- a/src/app/workspaces/workspace-details/environments/list-servers/list-servers.controller.ts +++ b/src/app/workspaces/workspace-details/environments/list-servers/list-servers.controller.ts @@ -213,7 +213,7 @@ export class ListServersController { content += 'this selected server?'; } - return this.confirmDialogService.showConfirmDialog('Remove servers', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove servers', content, { resolve: 'Delete' }); } } diff --git a/src/app/workspaces/workspace-details/environments/machine-config/machine-config.controller.ts b/src/app/workspaces/workspace-details/environments/machine-config/machine-config.controller.ts index 81fcdecd891..2ae57583b6e 100644 --- a/src/app/workspaces/workspace-details/environments/machine-config/machine-config.controller.ts +++ b/src/app/workspaces/workspace-details/environments/machine-config/machine-config.controller.ts @@ -232,7 +232,7 @@ export class WorkspaceMachineConfigController { deleteMachine($event: MouseEvent): void { let promise; if (!this.machineConfig.isDev) { - promise = this.confirmDialogService.showConfirmDialog('Remove container', 'Would you like to delete this container?', 'Delete'); + promise = this.confirmDialogService.showConfirmDialog('Remove container', 'Would you like to delete this container?', { resolve: 'Delete' }); } else { promise = this.showDeleteDevMachineDialog($event); } diff --git a/src/app/workspaces/workspace-details/list-commands/list-commands.controller.ts b/src/app/workspaces/workspace-details/list-commands/list-commands.controller.ts index d9b280f930d..84c3ed26b75 100644 --- a/src/app/workspaces/workspace-details/list-commands/list-commands.controller.ts +++ b/src/app/workspaces/workspace-details/list-commands/list-commands.controller.ts @@ -205,6 +205,6 @@ export class ListCommandsController { content += 'this selected command?'; } - return this.confirmDialogService.showConfirmDialog('Remove commands', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove commands', content, { resolve: 'Delete' }); } } diff --git a/src/app/workspaces/workspace-details/workspace-details.controller.ts b/src/app/workspaces/workspace-details/workspace-details.controller.ts index 5580f93f22f..f7280ea8b97 100644 --- a/src/app/workspaces/workspace-details/workspace-details.controller.ts +++ b/src/app/workspaces/workspace-details/workspace-details.controller.ts @@ -13,10 +13,10 @@ import {CheWorkspace, WorkspaceStatus} from '../../../components/api/workspace/che-workspace.factory'; import {CheNotification} from '../../../components/notification/che-notification.factory'; import {WorkspaceDetailsService} from './workspace-details.service'; -import IdeSvc from '../../ide/ide.service'; import {WorkspacesService} from '../workspaces.service'; import {ICheEditModeOverlayConfig} from '../../../components/widget/edit-mode-overlay/che-edit-mode-overlay.directive'; import {CheBranding} from '../../../components/branding/che-branding.factory'; +import {WorkspaceDataManager} from '../../../components/api/workspace/workspace-data-manager'; export interface IInitData { namespaceId: string; @@ -34,23 +34,33 @@ export interface IInitData { */ export class WorkspaceDetailsController { - static $inject = ['$location', '$log', '$sce', '$scope', 'lodash', 'cheNotification', 'cheWorkspace', 'ideSvc', 'workspaceDetailsService', 'initData', '$timeout', 'cheBranding', 'workspacesService']; + static $inject = [ + '$location', + '$q', + '$sce', + '$scope', + '$timeout', + 'cheNotification', + 'cheWorkspace', + 'workspaceDetailsService', + 'initData', + 'cheBranding', + 'workspacesService' + ]; /** * Overlay panel configuration. */ - editOverlayConfig: ICheEditModeOverlayConfig; - workspaceDetails: che.IWorkspace; - workspacesService: WorkspacesService; - private lodash: any; + private editOverlayConfig: ICheEditModeOverlayConfig; + private workspaceDetails: che.IWorkspace; + private workspacesService: WorkspacesService; + private $q: ng.IQService; private $scope: ng.IScope; private $sce: ng.ISCEService; - private $log: ng.ILogService; private $location: ng.ILocationService; private $timeout: ng.ITimeoutService; private cheNotification: CheNotification; private cheWorkspace: CheWorkspace; - private ideSvc: IdeSvc; private workspaceDetailsService: WorkspaceDetailsService; private loading: boolean = false; private selectedTabIndex: number; @@ -58,58 +68,55 @@ export class WorkspaceDetailsController { private workspaceId: string = ''; private workspaceName: string = ''; private newName: string = ''; - private originWorkspaceDetails: any = {}; + private initialWorkspaceDetails: che.IWorkspace = {}; private workspaceImportedRecipe: che.IRecipe; private forms: Map = new Map(); private tab: { [key: string]: string } = {}; private errorMessage: string = ''; + private failedTabs: string[] = []; private tabsValidationTimeout: ng.IPromise; private pluginRegistry: string; private TAB: Array; private cheBranding: CheBranding; + private workspaceDataManager: WorkspaceDataManager; /** - * There are unsaved changes to apply (with restart) when is't true. - */ - private unsavedChangesToApply: boolean; - /** - * There is selected deprecated editor when is't true. + * There is selected deprecated editor when it's true. */ private hasSelectedDeprecatedEditor: boolean; /** - * There are selected deprecated plugins when is't true. + * There are selected deprecated plugins when it's true. */ private hasSelectedDeprecatedPlugins: boolean; /** * Default constructor that is using resource injection */ - constructor($location: ng.ILocationService, - $log: ng.ILogService, - $sce: ng.ISCEService, - $scope: ng.IScope, - lodash: any, - cheNotification: CheNotification, - cheWorkspace: CheWorkspace, - ideSvc: IdeSvc, - workspaceDetailsService: WorkspaceDetailsService, - initData: IInitData, - $timeout: ng.ITimeoutService, - cheBranding: CheBranding, - workspacesService: WorkspacesService) { - this.$log = $log; + constructor( + $location: ng.ILocationService, + $q: ng.IQService, + $sce: ng.ISCEService, + $scope: ng.IScope, + $timeout: ng.ITimeoutService, + cheNotification: CheNotification, + cheWorkspace: CheWorkspace, + workspaceDetailsService: WorkspaceDetailsService, + initData: IInitData, + cheBranding: CheBranding, + workspacesService: WorkspacesService + ) { + this.$location = $location; + this.$q = $q; this.$sce = $sce; this.$scope = $scope; this.$timeout = $timeout; - this.$location = $location; - this.lodash = lodash; this.cheNotification = cheNotification; this.cheWorkspace = cheWorkspace; - this.ideSvc = ideSvc; this.workspaceDetailsService = workspaceDetailsService; this.workspacesService = workspacesService; this.cheBranding = cheBranding; this.pluginRegistry = cheWorkspace.getWorkspaceSettings() != null ? cheWorkspace.getWorkspaceSettings().cheWorkspacePluginRegistryUrl : null; + this.workspaceDataManager = this.cheWorkspace.getWorkspaceDataManager(); if (!initData.workspaceDetails) { cheNotification.showError(`There is no workspace with name ${initData.workspaceName}`); @@ -122,22 +129,24 @@ export class WorkspaceDetailsController { this.workspaceId = initData.workspaceDetails.id; const action = (newWorkspaceDetails: che.IWorkspace) => { - if (angular.equals(newWorkspaceDetails, this.originWorkspaceDetails)) { + if (this.initialWorkspaceDetails.config && angular.equals(newWorkspaceDetails.config, this.initialWorkspaceDetails.config)) { + return; + } else if (this.initialWorkspaceDetails.devfile && angular.equals(newWorkspaceDetails.devfile, this.initialWorkspaceDetails.devfile)) { return; } - this.originWorkspaceDetails = angular.copy(newWorkspaceDetails); - if (this.unsavedChangesToApply === false) { + this.initialWorkspaceDetails = angular.copy(newWorkspaceDetails); + if (this.workspaceDetailsService.isWorkspaceModified(this.workspaceId)) { this.workspaceDetails = angular.copy(newWorkspaceDetails); + this.workspaceName = this.workspaceDataManager.getName(this.workspaceDetails); } this.checkEditMode(); this.updateDeprecatedInfo(); }; this.cheWorkspace.subscribeOnWorkspaceChange(initData.workspaceDetails.id, action); - this.originWorkspaceDetails = angular.copy(initData.workspaceDetails); + this.initialWorkspaceDetails = angular.copy(initData.workspaceDetails); this.workspaceDetails = angular.copy(initData.workspaceDetails); - this.checkEditMode(); this.updateDeprecatedInfo(); this.TAB = this.workspaceDetails.config ? ['Overview', 'Projects', 'Containers', 'Servers', 'Env_Variables', 'Volumes', 'Config', 'SSH', 'Plugins', 'Editors'] : ['Overview', 'Projects', 'Plugins', 'Editors', 'Devfile']; this.updateTabs(); @@ -150,36 +159,48 @@ export class WorkspaceDetailsController { this.updateSelectedTab(tab); } }, true); + const failedTabsDeregistrationFn = $scope.$watch(() => { + return this.checkForFailedTabs(); + }, () => { + const isSaved = this.workspaceDetailsService.isWorkspaceConfigSaved(this.workspaceId); + this.updateEditModeOverlayConfig(isSaved === false); + }, true); $scope.$on('$destroy', () => { this.cheWorkspace.unsubscribeOnWorkspaceChange(this.workspaceId, action); searchDeRegistrationFn(); + failedTabsDeregistrationFn(); }); this.editOverlayConfig = { visible: false, disabled: false, message: { - content: this.getOverlayMessage(), + content: '', visible: false }, applyButton: { action: () => { this.applyConfigChanges(); }, - disabled: this.workspaceDetailsService.getRestartToApply(this.workspaceId) === false, + disabled: true, title: 'Apply' }, saveButton: { action: () => { - this.saveConfigChanges(true); + this.saveConfigChanges(); }, title: 'Save', - disabled: false + disabled: true }, cancelButton: { action: () => { this.cancelConfigChanges(); } + }, + preventPageLeave: false, + onChangesDiscard: (): ng.IPromise => { + this.cancelConfigChanges(); + return this.$q.when(); } }; } @@ -277,85 +298,6 @@ export class WorkspaceDetailsController { return this.workspaceDetailsService.getPages(); } - /** - * Returns workspace details section. - * - * @returns {*} - */ - getSections(): any { - return this.workspaceDetailsService.getSections(); - } - - /** - * Callback when workspace config has been changed in editor. - * - * @param config {che.IWorkspaceConfig} workspace config - */ - updateWorkspaceConfigImport(config: che.IWorkspaceConfig): void { - if (!config) { - return; - } - if (angular.equals(this.workspaceDetails.config, config)) { - return; - } - if (this.newName !== config.name) { - this.newName = config.name; - } - - if (config.defaultEnv && config.environments[config.defaultEnv]) { - this.workspaceImportedRecipe = config.environments[config.defaultEnv].recipe; - } else if (Object.keys(config.environments).length > 0) { - return; - } - - this.workspaceDetails.config = config; - - - if (!this.originWorkspaceDetails || !this.workspaceDetails) { - return; - } - - // check for failed tabs - const failedTabs = this.checkForFailedTabs(); - // publish changes - this.workspaceDetailsService.publishWorkspaceChange(this.workspaceDetails); - - if (!failedTabs || failedTabs.length === 0) { - let runningWorkspace = this.getWorkspaceStatus() === WorkspaceStatus[WorkspaceStatus.STARTING] || this.getWorkspaceStatus() === WorkspaceStatus[WorkspaceStatus.RUNNING]; - this.saveConfigChanges(false, runningWorkspace); - } - } - - /** - * Callback when workspace devfile has been changed in editor. - * - * @param {che.IWorkspaceDevfile} devfile workspace devfile - */ - updateWorkspaceDevfile(devfile: che.IWorkspaceDevfile): void { - if (!devfile) { - return; - } - if (angular.equals(this.workspaceDetails.devfile, devfile)) { - return; - } - - if (this.newName !== devfile.metadata.name) { - this.newName = devfile.metadata.name; - } - - this.workspaceDetails.devfile = devfile; - - - if (!this.originWorkspaceDetails || !this.workspaceDetails) { - return; - } - - this.workspaceDetailsService.publishWorkspaceChange(this.workspaceDetails); - - let runningWorkspace = this.getWorkspaceStatus() === WorkspaceStatus[WorkspaceStatus.STARTING] || this.getWorkspaceStatus() === WorkspaceStatus[WorkspaceStatus.RUNNING]; - this.saveConfigChanges(false, runningWorkspace); - } - /** * This method checks form validity on each tab and returns true if * all forms are valid. @@ -363,26 +305,29 @@ export class WorkspaceDetailsController { * @returns {string[]} list of names of failed tabs. */ checkForFailedTabs(): string[] { - const failTabs = []; const tabs = Object.keys(this.tab).filter((tabKey: string) => { return !isNaN(parseInt(tabKey, 10)); }); tabs.forEach((tabKey: string) => { - if (this.checkFormsNotValid(tabKey)) { - failTabs.push(this.tab[tabKey]); + const tabNotValid = this.checkFormsNotValid(tabKey); + const tabName = this.tab[tabKey]; + const index = this.failedTabs.indexOf(tabName); + if (tabNotValid && index === -1) { + this.failedTabs.push(tabName); + } + if (tabNotValid === false && index !== -1) { + this.failedTabs.splice(index, 1); } }); - - return failTabs; + return this.failedTabs; } /** * Builds and returns message for edit-mode-overlay component. * - * @param {string[]=} failedTabs list of names of failed tabs. * @returns {string} */ - getOverlayMessage(failedTabs?: string[]): string { + getOverlayMessage(): string { if (!this.isSupportedRecipeType) { return `Current infrastructure doesn't support this workspace recipe type.`; } @@ -391,11 +336,11 @@ export class WorkspaceDetailsController { return `This workspace is using old definition format which is not compatible anymore.`; } - if (failedTabs && failedTabs.length > 0) { + if (this.failedTabs.length > 0) { const url = this.$location.absUrl().split('?')[0]; let message = ` Impossible to save and apply the configuration. Errors in `; - message += failedTabs.map((tab: string) => { + message += this.failedTabs.map((tab: string) => { return `${tab}`; }).join(', '); @@ -407,42 +352,42 @@ export class WorkspaceDetailsController { /** * Updates config of edit-mode-overlay component. - * - * @param {boolean} configIsDiffer true if config is differ - * @param {string[]=} failedTabs list of names of failed tabs. */ - updateEditModeOverlayConfig(configIsDiffer: boolean, failedTabs?: string[]): void { - const formIsValid = !failedTabs || failedTabs.length === 0; + updateEditModeOverlayConfig(workspaceIsModified: boolean): void { + // check for failed tabs + const formIsValid = !this.failedTabs || this.failedTabs.length === 0; // panel this.editOverlayConfig.disabled = !formIsValid || this.loading; - this.editOverlayConfig.visible = configIsDiffer || this.workspaceDetailsService.getRestartToApply(this.workspaceId); + this.editOverlayConfig.visible = workspaceIsModified || this.workspaceDetailsService.doesWorkspaceConfigNeedRestart(this.workspaceId) === true; // 'save' button - this.editOverlayConfig.saveButton.disabled = !this.isSupported || !configIsDiffer; + const saveButtonDisabled = !this.isSupported || !workspaceIsModified; + this.editOverlayConfig.saveButton.disabled = saveButtonDisabled; // 'apply' button this.editOverlayConfig.applyButton.disabled = !this.isSupported - || (!this.unsavedChangesToApply && !this.workspaceDetailsService.getRestartToApply(this.workspaceId)); + || (this.workspaceDetailsService.doesWorkspaceConfigNeedRestart(this.workspaceId) === false); // 'cancel' button - this.editOverlayConfig.cancelButton.disabled = !configIsDiffer; + this.editOverlayConfig.cancelButton.disabled = !workspaceIsModified; // message content - this.editOverlayConfig.message.content = this.getOverlayMessage(failedTabs); + this.editOverlayConfig.message.content = this.getOverlayMessage(); // message visibility this.editOverlayConfig.message.visible = !this.isSupported - || failedTabs.length > 0 - || this.unsavedChangesToApply - || this.workspaceDetailsService.getRestartToApply(this.workspaceId); + || this.failedTabs.length > 0 + || this.workspaceDetailsService.doesWorkspaceConfigNeedRestart(this.workspaceId) === true; + + this.editOverlayConfig.preventPageLeave = saveButtonDisabled === false; } /** * Checks editing mode for workspace config. */ - checkEditMode(restartToApply?: boolean): ng.IPromise { - if (!this.originWorkspaceDetails || !this.workspaceDetails) { + checkEditMode(): ng.IPromise { + if (!this.initialWorkspaceDetails || !this.workspaceDetails) { return; } @@ -451,24 +396,40 @@ export class WorkspaceDetailsController { } return this.tabsValidationTimeout = this.$timeout(() => { - const configIsDiffer = this.originWorkspaceDetails.config ? !angular.equals(this.originWorkspaceDetails.config, this.workspaceDetails.config) : !angular.equals(this.originWorkspaceDetails.devfile, this.workspaceDetails.devfile); + this.onWorkspaceChanged(); + }, 500); + } - // the workspace should be restarted only if its status is STARTING or RUNNING - if (this.getWorkspaceStatus() === WorkspaceStatus[WorkspaceStatus.STARTING] || this.getWorkspaceStatus() === WorkspaceStatus[WorkspaceStatus.RUNNING]) { - this.unsavedChangesToApply = configIsDiffer && (this.unsavedChangesToApply || !!restartToApply); - } else { - this.unsavedChangesToApply = false; - } + onWorkspaceChanged(): void { + let isModified: boolean; + let needRestart: boolean; + if (this.initialWorkspaceDetails.config) { + ({ isModified, needRestart } = this.isModifiedConfig()); + } else { + ({ isModified, needRestart } = this.isModifiedDevfile()); + } - // check for failed tabs - const failedTabs = this.checkForFailedTabs(); - // update overlay - this.updateEditModeOverlayConfig(configIsDiffer, failedTabs); - // update info(editor and plugins) - this.updateDeprecatedInfo(); - // publish changes - this.workspaceDetailsService.publishWorkspaceChange(this.workspaceDetails); - }, 500); + if (this.getWorkspaceStatus() === WorkspaceStatus[WorkspaceStatus.STARTING] + || this.getWorkspaceStatus() === WorkspaceStatus[WorkspaceStatus.RUNNING]) { + needRestart = needRestart || this.workspaceDetailsService.doesWorkspaceConfigNeedRestart(this.workspaceId); + } else { + needRestart = false; + } + + if (isModified || needRestart) { + this.workspaceDetailsService.setModified(this.workspaceId, { isSaved: isModified === false, needRestart }); + } else { + this.workspaceDetailsService.removeModified(this.workspaceId); + } + + // update overlay + this.updateEditModeOverlayConfig(isModified); + + // update info(editor and plugins) + this.updateDeprecatedInfo(); + + // publish changes + this.workspaceDetailsService.publishWorkspaceChange(this.workspaceDetails); } /** @@ -478,27 +439,27 @@ export class WorkspaceDetailsController { this.editOverlayConfig.disabled = true; this.loading = true; - this.$scope.$broadcast('edit-workspace-details', {status: 'saving'}); + this.$scope.$broadcast('edit-workspace-details', { status: 'saving' }); this.workspaceDetailsService.applyConfigChanges(this.workspaceDetails) .then(() => { - this.workspaceDetailsService.removeRestartToApply(this.workspaceId); - this.unsavedChangesToApply = false; - + this.workspaceDetailsService.removeModified(this.workspaceId); this.cheNotification.showInfo('Workspace updated.'); - this.$scope.$broadcast('edit-workspace-details', {status: 'saved'}); - - return this.cheWorkspace.fetchWorkspaceDetails(this.originWorkspaceDetails.id).then(() => { - this.$location.path('/workspace/' + this.namespaceId + '/' + this.workspaceDetails.config.name).search({tab: this.tab[this.selectedTabIndex]}); - }); + this.$scope.$broadcast('edit-workspace-details', { status: 'saved' }); }) .catch((error: any) => { - this.$scope.$broadcast('edit-workspace-details', {status: 'failed'}); + this.$scope.$broadcast('edit-workspace-details', { status: 'failed' }); this.cheNotification.showError('Update workspace failed.', error); - return this.checkEditMode(true); + }) + .then(() => { + return this.cheWorkspace.fetchWorkspaceDetails(this.initialWorkspaceDetails.id); + }) + .then(() => { + this.$location.path('/workspace/' + this.namespaceId + '/' + this.workspaceDataManager.getName(this.workspaceDetails)).search({ tab: this.tab[this.selectedTabIndex] }); }) .finally(() => { this.loading = false; + return this.onWorkspaceChanged(); }); } @@ -506,43 +467,33 @@ export class WorkspaceDetailsController { * Updates workspace with new config. * */ - saveConfigChanges(refreshPage: boolean, notifyRestart?: boolean): void { + saveConfigChanges(): void { + const notifyRestart = this.getWorkspaceStatus() === WorkspaceStatus[WorkspaceStatus.STARTING] + || this.getWorkspaceStatus() === WorkspaceStatus[WorkspaceStatus.RUNNING]; + this.editOverlayConfig.disabled = true; this.loading = true; - this.$scope.$broadcast('edit-workspace-details', {status: 'saving'}); + this.$scope.$broadcast('edit-workspace-details', { status: 'saving' }); this.workspaceDetailsService.saveConfigChanges(this.workspaceDetails) .then(() => { - if (this.unsavedChangesToApply) { - this.workspaceDetailsService.addRestartToApply(this.workspaceId); - } else { - this.workspaceDetailsService.removeRestartToApply(this.workspaceId); - } - this.unsavedChangesToApply = false; + this.workspaceDetailsService.setModified(this.workspaceId, { isSaved: true }); let message = 'Workspace updated.'; message += notifyRestart ? '
To apply changes in running workspace - need to restart it.' : ''; this.cheNotification.showInfo(message); - this.$scope.$broadcast('edit-workspace-details', {status: 'saved'}); - - if (refreshPage) { - return this.cheWorkspace.fetchWorkspaceDetails(this.originWorkspaceDetails.id).then(() => { - let name = this.cheWorkspace.getWorkspaceDataManager().getName(this.workspaceDetails); - this.$location.path('/workspace/' + this.namespaceId + '/' + name).search({tab: this.tab[this.selectedTabIndex]}); - }); - } + this.$scope.$broadcast('edit-workspace-details', { status: 'saved' }); }) .catch((error: any) => { - this.$scope.$broadcast('edit-workspace-details', {status: 'failed'}); - const errorMessage = 'Cannot update workspace configuration.'; + this.$scope.$broadcast('edit-workspace-details', { status: 'failed' }); + const errorMessage = 'Cannot retrieve workspace configuration.'; this.cheNotification.showError(error && error.data && error.data.message ? error.data.message : errorMessage); }) .finally(() => { this.loading = false; - - return this.checkEditMode(); + return this.onWorkspaceChanged(); }); } @@ -550,25 +501,27 @@ export class WorkspaceDetailsController { * Cancels workspace config changes that weren't stored */ cancelConfigChanges(): void { - this.editOverlayConfig.disabled = true; - this.unsavedChangesToApply = false; - - this.workspaceDetails = angular.copy(this.originWorkspaceDetails); - - this.checkEditMode(); - - this.$scope.$broadcast('edit-workspace-details', {status: 'cancelled'}); + this.workspaceDetailsService.removeModified(this.workspaceId); + this.workspaceDetails = angular.copy(this.initialWorkspaceDetails); + this.onWorkspaceChanged(); + this.$scope.$broadcast('edit-workspace-details', { status: 'cancelled' }); } - runWorkspace(): ng.IPromise { + runWorkspace(): ng.IPromise { this.errorMessage = ''; + if (this.workspaceDetailsService.isWorkspaceModified(this.workspaceId)) { + return this.workspaceDetailsService.notifyUnsavedChangesDialog(); + } return this.workspaceDetailsService.runWorkspace(this.workspaceDetails).catch((error: any) => { this.errorMessage = error.message; }); } - stopWorkspace(): ng.IPromise { + stopWorkspace(): ng.IPromise { + if (this.workspaceDetailsService.isWorkspaceModified(this.workspaceId)) { + return this.workspaceDetailsService.notifyUnsavedChangesDialog(); + } return this.workspaceDetailsService.stopWorkspace(this.workspaceDetails.id); } @@ -593,17 +546,38 @@ export class WorkspaceDetailsController { return form && form.$invalid; } - /** - * Returns true when 'Save' button should be disabled - * - * @returns {boolean} - */ - isSaveButtonDisabled(): boolean { - const tabs = Object.keys(this.tab).filter((tabKey: string) => { - return !isNaN(parseInt(tabKey, 10)); - }); + private isModifiedConfig(): { isModified: boolean, needRestart: boolean } { + const isEqual = angular.equals(this.initialWorkspaceDetails.config, this.workspaceDetails.config); + if (isEqual) { + return { + isModified: false, + needRestart: false + }; + } - return tabs.some((tabKey: string) => this.checkFormsNotValid(tabKey)); + const tmpConfig = angular.extend({}, this.initialWorkspaceDetails.config, { name: this.workspaceDetails.config.name }); + const needRestart = false === angular.equals(tmpConfig, this.workspaceDetails.config); + return { + isModified: true, + needRestart + }; + } + + private isModifiedDevfile(): { isModified: boolean, needRestart: boolean } { + const isEqual = angular.equals(this.initialWorkspaceDetails.devfile, this.workspaceDetails.devfile); + if (isEqual) { + return { + isModified: false, + needRestart: false + }; + } + + const tmpDevfile = angular.extend({}, this.initialWorkspaceDetails.devfile, { metadata: { name: this.workspaceDetails.devfile.metadata.name } }); + const needRestart = false === angular.equals(tmpDevfile, this.workspaceDetails.devfile); + return { + isModified: true, + needRestart + }; } /** @@ -638,4 +612,5 @@ export class WorkspaceDetailsController { const deprecatedPlugins = this.workspaceDetailsService.getSelectedDeprecatedPlugins(this.workspaceDetails); this.hasSelectedDeprecatedPlugins = deprecatedPlugins.length > 0; } + } diff --git a/src/app/workspaces/workspace-details/workspace-details.directive.spec.ts b/src/app/workspaces/workspace-details/workspace-details.directive.spec.ts index 843b81ff6b5..3a4c786db00 100644 --- a/src/app/workspaces/workspace-details/workspace-details.directive.spec.ts +++ b/src/app/workspaces/workspace-details/workspace-details.directive.spec.ts @@ -281,12 +281,14 @@ describe(`WorkspaceDetailsController >`, () => { angular.mock.module('workspaceDetailsMock'); }); - beforeEach(inject((_$rootScope_: ng.IRootScopeService, - _$compile_: ng.ICompileService, - _cheHttpBackend_: CheHttpBackend, - _$timeout_: ng.ITimeoutService, - _$q_: ng.IQService, - _cheWorkspace_: CheWorkspace) => { + beforeEach(inject(( + _$rootScope_: ng.IRootScopeService, + _$compile_: ng.ICompileService, + _cheHttpBackend_: CheHttpBackend, + _$timeout_: ng.ITimeoutService, + _$q_: ng.IQService, + _cheWorkspace_: CheWorkspace + ) => { $scope = _$rootScope_.$new(); $compile = _$compile_; $timeout = _$timeout_; @@ -320,6 +322,12 @@ describe(`WorkspaceDetailsController >`, () => { $httpBackend.verifyNoOutstandingRequest(); }); + afterEach(() => { + compiledDirective = undefined; + cheWorkspace = undefined; + newWorkspace = undefined; + }); + describe(`overflow panel >`, () => { function getOverlayPanelEl(): ng.IAugmentedJQuery { @@ -335,9 +343,20 @@ describe(`WorkspaceDetailsController >`, () => { return compiledDirective.find('.cancel-button button'); } - it(`should be hidden initially >`, () => { - compileDirective(); - expect(getOverlayPanelEl().children().length).toEqual(0); + describe('initially >', () => { + + beforeEach(() => { + compileDirective(); + }); + + it(`should be hidden >`, () => { + expect(getOverlayPanelEl().children().length).toEqual(0); + }); + + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); + }); + }); describe(`when config is changed >`, () => { @@ -349,12 +368,16 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { compileDirective(); - controller.workspaceDetails.config.name = 'wksp-new-name'; - controller.checkEditMode(false); + (controller as any).workspaceDetails.config.name = 'wksp-new-name'; + controller.checkEditMode(); $scope.$digest(); $timeout.flush(); }); + it('should prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeTruthy(); + }); + it(`the overflow panel should be shown >`, () => { expect(getOverlayPanelEl().length).toEqual(1); }); @@ -376,7 +399,10 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { getCancelButton().click(); $scope.$digest(); - $timeout.flush(); + }); + + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); }); it(`the overlay panel should be hidden >`, () => { @@ -389,13 +415,17 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { // set new workspace to publish - newWorkspace = angular.copy(controller.workspaceDetails); + newWorkspace = angular.copy((controller as any).workspaceDetails); getSaveButton().click(); $scope.$digest(); $timeout.flush(); }); + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); + }); + it(`the overlay panel should be hidden >`, () => { expect(getOverlayPanelEl().children().length).toEqual(0); }); @@ -409,12 +439,16 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { compileDirective(); - controller.workspaceDetails.config.name = 'wksp-new-name'; - controller.checkEditMode(true); + (controller as any).workspaceDetails.config.defaultEnv = 'new-env'; + controller.checkEditMode(); $scope.$digest(); $timeout.flush(); }); + it('should prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeTruthy(); + }); + it(`the overflow panel should be shown >`, () => { expect(getOverlayPanelEl().length).toEqual(1); }); @@ -436,7 +470,10 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { getCancelButton().click(); $scope.$digest(); - $timeout.flush(); + }); + + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); }); it(`the overlay panel should be hidden >`, () => { @@ -448,11 +485,16 @@ describe(`WorkspaceDetailsController >`, () => { describe(`and saveButton is clicked >`, () => { beforeEach(() => { + newWorkspace = angular.copy((controller as any).workspaceDetails); getSaveButton().click(); $scope.$digest(); $timeout.flush(); }); + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); + }); + it(`the overlay panel should remain visible >`, () => { expect(getOverlayPanelEl().length).toEqual(1); }); @@ -475,13 +517,17 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { // set new workspace to publish - newWorkspace = angular.copy(controller.workspaceDetails); + newWorkspace = angular.copy((controller as any).workspaceDetails); getApplyButton().click(); $scope.$digest(); $timeout.flush(); }); + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); + }); + it(`the overlay panel should be hidden >`, () => { expect(getOverlayPanelEl().children().length).toEqual(0); }); @@ -498,17 +544,21 @@ describe(`WorkspaceDetailsController >`, () => { compileDirective(); controller.stopWorkspace(); - controller.workspaceDetails.config.name = 'wksp-new-name'; + (controller as any).workspaceDetails.config.name = 'wksp-new-name'; }); describe(`and restart is not necessary >`, () => { beforeEach(() => { - controller.checkEditMode(false); + controller.checkEditMode(); $scope.$digest(); $timeout.flush(); }); + it('should prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeTruthy(); + }); + it(`the overflow panel should be shown >`, () => { expect(getOverlayPanelEl().length).toEqual(1); }); @@ -530,7 +580,10 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { getCancelButton().click(); $scope.$digest(); - $timeout.flush(); + }); + + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); }); it(`the overlay panel should be hidden >`, () => { @@ -543,13 +596,17 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { // set new workspace to publish - newWorkspace = angular.copy(controller.workspaceDetails); + newWorkspace = angular.copy((controller as any).workspaceDetails); getSaveButton().click(); $scope.$digest(); $timeout.flush(); }); + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); + }); + it(`the overlay panel should be hidden >`, () => { expect(getOverlayPanelEl().children().length).toEqual(0); }); @@ -561,11 +618,15 @@ describe(`WorkspaceDetailsController >`, () => { describe(`and restart is necessary >`, () => { beforeEach(() => { - controller.checkEditMode(true); + controller.checkEditMode(); $scope.$digest(); $timeout.flush(); }); + it('should prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeTruthy(); + }); + it(`the overflow panel should be shown >`, () => { expect(getOverlayPanelEl().length).toEqual(1); }); @@ -587,7 +648,10 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { getCancelButton().click(); $scope.$digest(); - $timeout.flush(); + }); + + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); }); it(`the overlay panel should be hidden >`, () => { @@ -600,13 +664,17 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { // set new workspace to publish - newWorkspace = angular.copy(controller.workspaceDetails); + newWorkspace = angular.copy((controller as any).workspaceDetails); getSaveButton().click(); $scope.$digest(); $timeout.flush(); }); + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); + }); + it(`the overlay panel should be hidden >`, () => { expect(getOverlayPanelEl().children().length).toEqual(0); }); @@ -622,23 +690,27 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { compileDirective(); - controller.workspacesService.isSupported = jasmine.createSpy('workspaceDetailsController.isSupported') + (controller as any).workspacesService.isSupported = jasmine.createSpy('workspaceDetailsController.isSupported') .and .callFake(() => { return false; }); - controller.workspacesService.isSupportedRecipeType = jasmine.createSpy('workspaceDetailsController.isSupportedRecipeType') + (controller as any).workspacesService.isSupportedRecipeType = jasmine.createSpy('workspaceDetailsController.isSupportedRecipeType') .and .callFake(() => { return false; }); - controller.workspaceDetails.config.name = 'wksp-new-name'; - controller.checkEditMode(false); + (controller as any).workspaceDetails.config.name = 'wksp-new-name'; + controller.checkEditMode(); $scope.$digest(); $timeout.flush(); }); + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); + }); + it(`the overflow panel should be shown >`, () => { expect(getOverlayPanelEl().length).toEqual(1); }); @@ -660,7 +732,10 @@ describe(`WorkspaceDetailsController >`, () => { beforeEach(() => { getCancelButton().click(); $scope.$digest(); - $timeout.flush(); + }); + + it('should not prevent to leave page', () => { + expect((controller as any).editOverlayConfig.preventPageLeave).toBeFalsy(); }); it(`the overlay panel should be hidden >`, () => { diff --git a/src/app/workspaces/workspace-details/workspace-details.html b/src/app/workspaces/workspace-details/workspace-details.html index 3679ead5066..3f0e2f07d0d 100644 --- a/src/app/workspaces/workspace-details/workspace-details.html +++ b/src/app/workspaces/workspace-details/workspace-details.html @@ -42,7 +42,7 @@ @@ -56,7 +56,7 @@ + projects-on-change="workspaceDetailsController.checkEditMode()"> @@ -68,7 +68,7 @@ + on-change="workspaceDetailsController.checkEditMode()"> @@ -81,7 +81,7 @@ + on-change="workspaceDetailsController.checkEditMode()"> @@ -98,7 +98,7 @@ + on-change="workspaceDetailsController.checkEditMode()"> @@ -115,7 +115,7 @@ + on-change="workspaceDetailsController.checkEditMode()"> @@ -167,7 +167,7 @@ + on-change="workspaceDetailsController.checkEditMode()"> @@ -183,31 +183,31 @@ + on-change="workspaceDetailsController.checkEditMode()"> - - - - - Devfile - - - - - - - - - + + + + + Devfile + + + + + + + + + (); @@ -288,12 +339,11 @@ export class WorkspaceDetailsService { return this.cheWorkspace.fetchStatusChange(workspace.id, WorkspaceStatus[WorkspaceStatus.STOPPED]); }) .then(() => { - this.removeRestartToApply(workspace.id); - return this.saveConfigChanges(workspace); }) .then(() => { - this.cheWorkspace.startWorkspace(workspace.id, workspace.config.defaultEnv); + const envName = workspace.config ? workspace.config.defaultEnv : undefined; + this.cheWorkspace.startWorkspace(workspace.id, envName); return this.cheWorkspace.fetchStatusChange(workspace.id, WorkspaceStatus[WorkspaceStatus.RUNNING]); }) .catch((error: any) => { @@ -303,39 +353,39 @@ export class WorkspaceDetailsService { } /** - * Add workspace ID to the list of workspaces that should be restarted. - * - * @param {string} workspaceId + * Keep a workspace as one that is modified and may need restarting to apply changes. */ - addRestartToApply(workspaceId: string): void { - if (this.restartToApply.indexOf(workspaceId) === -1) { - this.restartToApply.push(workspaceId); - } + setModified(id: string, attrs: { isSaved?: boolean, needRestart?: boolean }): void { + this.modifiedWorkspaces.set(id, attrs); + } + + removeModified(id: string): void { + this.modifiedWorkspaces.remove(id); } /** - * Remove workspace ID from the list of workspaces that should be restarted. - * - * @param {string} workspaceId + * Returns `true` if workspace configuration has been already saved. + * @param id a workspace ID */ - removeRestartToApply(workspaceId: string): void { - const index = this.restartToApply.indexOf(workspaceId); - if (index === -1) { - return; - } - this.restartToApply.splice(index, 1); + isWorkspaceConfigSaved(id: string): boolean { + return this.modifiedWorkspaces.isSaved(id); } /** - * Returns true if workspace ID belongs to the list of - * workspaces that should be restarted. - * - * @param {string} workspaceId - * @returns {boolean} + * Returns `true` if workspace configuration has been already applied. + * @param id a workspace ID + */ + doesWorkspaceConfigNeedRestart(id: string): boolean { + return this.modifiedWorkspaces.needRestart(id); + } + + /** + * Returns `true` if workspace configuration was changed. + * @param id a workspace ID */ - getRestartToApply(workspaceId: string): boolean { - const index = this.restartToApply.indexOf(workspaceId); - return index !== -1; + isWorkspaceModified(id: string): boolean { + return this.modifiedWorkspaces.isSaved(id) === false + || this.modifiedWorkspaces.needRestart(id) === true; } /** @@ -413,4 +463,11 @@ export class WorkspaceDetailsService { }); } + /** + * Shows modal window with notification about unsaved changes. + */ + notifyUnsavedChangesDialog(): ng.IPromise { + return this.confirmDialogService.showConfirmDialog('Unsaved Changes', `You're editing this workspace configuration. Please save or discard changes to be able to run or stop the workspace.`, { reject: 'Close' }); + } + } diff --git a/src/app/workspaces/workspace-details/workspace-machine-env-variables/env-variables.controller.ts b/src/app/workspaces/workspace-details/workspace-machine-env-variables/env-variables.controller.ts index 1e80c10c239..b5a0dc0a516 100644 --- a/src/app/workspaces/workspace-details/workspace-machine-env-variables/env-variables.controller.ts +++ b/src/app/workspaces/workspace-details/workspace-machine-env-variables/env-variables.controller.ts @@ -190,7 +190,7 @@ export class EnvVariablesController { * @param variableName {string} */ deleteEnvVariable(variableName: string): void { - const promise = this.confirmDialogService.showConfirmDialog('Remove variable', 'Would you like to delete this variable?', 'Delete'); + const promise = this.confirmDialogService.showConfirmDialog('Remove variable', 'Would you like to delete this variable?', { resolve: 'Delete' }); promise.then(() => { delete this.envVariables[variableName]; this.environmentManager.setEnvVariables(this.selectedMachine, this.envVariables); @@ -212,6 +212,6 @@ export class EnvVariablesController { content += 'this selected variable?'; } - return this.confirmDialogService.showConfirmDialog('Remove variables', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove variables', content, { resolve: 'Delete' }); } } diff --git a/src/app/workspaces/workspace-details/workspace-machine-servers/machine-servers.controller.ts b/src/app/workspaces/workspace-details/workspace-machine-servers/machine-servers.controller.ts index 405f3b984fd..485f7f6b84c 100644 --- a/src/app/workspaces/workspace-details/workspace-machine-servers/machine-servers.controller.ts +++ b/src/app/workspaces/workspace-details/workspace-machine-servers/machine-servers.controller.ts @@ -204,7 +204,7 @@ export class MachineServersController { * @param reference {string} */ deleteServer(reference: string): void { - const promise = this.confirmDialogService.showConfirmDialog('Remove server', 'Would you like to delete this server?', 'Delete'); + const promise = this.confirmDialogService.showConfirmDialog('Remove server', 'Would you like to delete this server?', { resolve: 'Delete' }); promise.then(() => { delete this.servers[reference]; this.environmentManager.setServers(this.selectedMachine, this.servers); @@ -225,6 +225,6 @@ export class MachineServersController { content += 'this selected server?'; } - return this.confirmDialogService.showConfirmDialog('Remove servers', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove servers', content, { resolve: 'Delete' }); } } diff --git a/src/app/workspaces/workspace-details/workspace-machine-volumes/machine-volumes.controller.ts b/src/app/workspaces/workspace-details/workspace-machine-volumes/machine-volumes.controller.ts index 755924465c9..01a5cdec468 100644 --- a/src/app/workspaces/workspace-details/workspace-machine-volumes/machine-volumes.controller.ts +++ b/src/app/workspaces/workspace-details/workspace-machine-volumes/machine-volumes.controller.ts @@ -140,7 +140,7 @@ export class MachineVolumesController { * @param variableName {string} */ deleteMachineVolume(variableName: string): void { - const promise = this.confirmDialogService.showConfirmDialog('Remove variable', 'Would you like to delete this variable?', 'Delete'); + const promise = this.confirmDialogService.showConfirmDialog('Remove variable', 'Would you like to delete this variable?', { resolve: 'Delete' }); promise.then(() => { delete this.machineVolumes[variableName]; this.environmentManager.setMachineVolumes(this.selectedMachine, this.machineVolumes); @@ -162,6 +162,6 @@ export class MachineVolumesController { content += 'this selected variable?'; } - return this.confirmDialogService.showConfirmDialog('Remove variables', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove variables', content, { resolve: 'Delete' }); } } diff --git a/src/app/workspaces/workspace-details/workspace-machines/workspace-machines.controller.ts b/src/app/workspaces/workspace-details/workspace-machines/workspace-machines.controller.ts index 2dc648b0a1c..b2090fa9002 100644 --- a/src/app/workspaces/workspace-details/workspace-machines/workspace-machines.controller.ts +++ b/src/app/workspaces/workspace-details/workspace-machines/workspace-machines.controller.ts @@ -218,7 +218,7 @@ export class WorkspaceMachinesController { content += 'these ' + selectedItems.length + ' machines?'; } - return this.confirmDialogService.showConfirmDialog('Remove machines', content, 'Delete').then(() => { + return this.confirmDialogService.showConfirmDialog('Remove machines', content, { resolve: 'Delete' }).then(() => { return selectedItems; }); } @@ -276,7 +276,7 @@ export class WorkspaceMachinesController { * @param name {string} */ deleteMachine(name: string): void { - this.confirmDialogService.showConfirmDialog('Remove machine', 'Would you like to delete this machine?', 'Delete').then(() => { + this.confirmDialogService.showConfirmDialog('Remove machine', 'Would you like to delete this machine?', { resolve: 'Delete' }).then(() => { this.machineOnDelete(name); }); } diff --git a/src/app/workspaces/workspace-details/workspace-overview/workspace-details-overview.controller.ts b/src/app/workspaces/workspace-details/workspace-overview/workspace-details-overview.controller.ts index 9b0c2ccdd05..eb925b0372e 100644 --- a/src/app/workspaces/workspace-details/workspace-overview/workspace-details-overview.controller.ts +++ b/src/app/workspaces/workspace-details/workspace-overview/workspace-details-overview.controller.ts @@ -272,7 +272,7 @@ export class WorkspaceDetailsOverviewController { */ deleteWorkspace(): void { const content = 'Would you like to delete workspace \'' + this.cheWorkspace.getWorkspaceDataManager().getName(this.workspaceDetails) + '\'?'; - this.confirmDialogService.showConfirmDialog('Delete workspace', content, 'Delete').then(() => { + this.confirmDialogService.showConfirmDialog('Delete workspace', content, { resolve: 'Delete' }).then(() => { if ([RUNNING, STARTING].indexOf(this.getWorkspaceStatus()) !== -1) { this.cheWorkspace.stopWorkspace(this.workspaceDetails.id); } diff --git a/src/app/workspaces/workspace-details/workspace-overview/workspace-details-overview.html b/src/app/workspaces/workspace-details/workspace-overview/workspace-details-overview.html index 5fec462ccd7..4b67bdd7094 100644 --- a/src/app/workspaces/workspace-details/workspace-overview/workspace-details-overview.html +++ b/src/app/workspaces/workspace-details/workspace-overview/workspace-details-overview.html @@ -7,7 +7,7 @@ che-name="name" che-place-holder="Name of the workspace" aria-label="Name of the workspace" - ng-model-options="{ allowInvalid: true }" + ng-model-options="{ allowInvalid: true, debounce: 300 }" ng-model="workspaceDetailsOverviewController.name" che-on-change="workspaceDetailsOverviewController.onNameChange()" required diff --git a/src/app/workspaces/workspace-details/workspace-projects/workspace-details-projects.controller.ts b/src/app/workspaces/workspace-details/workspace-projects/workspace-details-projects.controller.ts index 6eb5e76bc6d..d3d372f28eb 100644 --- a/src/app/workspaces/workspace-details/workspace-projects/workspace-details-projects.controller.ts +++ b/src/app/workspaces/workspace-details/workspace-projects/workspace-details-projects.controller.ts @@ -256,7 +256,7 @@ export class WorkspaceDetailsProjectsCtrl { content += 'this selected project?'; } - return this.confirmDialogService.showConfirmDialog('Remove projects', content, 'Delete'); + return this.confirmDialogService.showConfirmDialog('Remove projects', content, { resolve: 'Delete' }); } workspaceIsRunning(): boolean { diff --git a/src/components/service/confirm-dialog/che-confirm-dialog.html b/src/components/service/confirm-dialog/che-confirm-dialog.html index 94b28a5db5e..8fb81804bb2 100644 --- a/src/components/service/confirm-dialog/che-confirm-dialog.html +++ b/src/components/service/confirm-dialog/che-confirm-dialog.html @@ -2,11 +2,12 @@
{{cheConfirmDialogController.content}}
- - diff --git a/src/components/service/confirm-dialog/confirm-dialog.service.ts b/src/components/service/confirm-dialog/confirm-dialog.service.ts index 7548966ea28..325d9559bed 100644 --- a/src/components/service/confirm-dialog/confirm-dialog.service.ts +++ b/src/components/service/confirm-dialog/confirm-dialog.service.ts @@ -34,12 +34,12 @@ export class ConfirmDialogService { * * @param title{string} popup title * @param content{string} dialog content - * @param resolveButtonTitle{string} title for resolve button - * @param rejectButtonTitle{string} title for reject button + * @param buttonTitles dialog buttons titles * * @returns {ng.IPromise} */ - showConfirmDialog(title: string, content: string, resolveButtonTitle: string, rejectButtonTitle?: string): ng.IPromise { + showConfirmDialog(title: string, content: string, buttonTitles?: { resolve?: string, reject?: string }): ng.IPromise { + buttonTitles.reject = buttonTitles.reject || 'Close'; return this.$mdDialog.show({ bindToController: true, clickOutsideToClose: true, @@ -49,8 +49,7 @@ export class ConfirmDialogService { content: content, $mdDialog: this.$mdDialog, title: title, - resolveButtonTitle: resolveButtonTitle, - rejectButtonTitle: rejectButtonTitle ? rejectButtonTitle : 'Close' + buttons: buttonTitles }, templateUrl: 'components/service/confirm-dialog/che-confirm-dialog.html' }); diff --git a/src/components/widget/edit-mode-overlay/che-edit-mode-overlay.controller.ts b/src/components/widget/edit-mode-overlay/che-edit-mode-overlay.controller.ts new file mode 100644 index 00000000000..d76d3afe87e --- /dev/null +++ b/src/components/widget/edit-mode-overlay/che-edit-mode-overlay.controller.ts @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2015-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +'use strict'; + +import { ICheEditModeOverlayConfig } from './che-edit-mode-overlay.directive'; +import { ConfirmDialogService } from '../../service/confirm-dialog/confirm-dialog.service'; + +export class CheEditModeOverlayController { + + static $inject = [ + '$location', + '$scope', + 'confirmDialogService' + ]; + private $location: ng.ILocationService; + private $scope: ng.IScope; + private confirmDialogService: ConfirmDialogService; + + private config: ICheEditModeOverlayConfig; + + constructor( + $location: ng.ILocationService, + $scope: ng.IScope, + confirmDialogService: ConfirmDialogService + ) { + this.$location = $location; + this.$scope = $scope; + this.confirmDialogService = confirmDialogService; + + this.$scope.$on('$locationChangeStart', (event: ng.IAngularEvent, newUrl: string, oldUrl: string) => { + if (this.config && this.config.preventPageLeave === false) { + return; + } + + // check if path remains the same + const oldPath = this.extractPathname(oldUrl); + const newPath = this.extractPathname(newUrl); + if (oldPath === newPath) { + return; + } + + event.preventDefault(); + + if (typeof this.config.onChangesDiscard !== 'function') { + return; + } + return this.discardUnsavedChangesDialog().then(() => { + return this.config.onChangesDiscard().then(() => { + const hash = newUrl.slice(newUrl.indexOf('#') + 1, newUrl.length); + this.$location.url(hash); + }); + }); + }); + } + + discardUnsavedChangesDialog(): ng.IPromise { + return this.confirmDialogService.showConfirmDialog('Unsaved Changes', 'You have unsaved changes. You may go ahead and discard all changes, or close this window and save them.', { resolve: 'Discard Changes', reject: 'Cancel' }); + } + + + /** + * Returns Angular's path + * @param url + */ + private extractPathname(url: string): string { + return url.slice(url.indexOf('#') + 1, url.indexOf('?') !== -1 ? url.indexOf('?') : url.length); + } + +} diff --git a/src/components/widget/edit-mode-overlay/che-edit-mode-overlay.directive.ts b/src/components/widget/edit-mode-overlay/che-edit-mode-overlay.directive.ts index bb7b613609f..7e0399ffd13 100644 --- a/src/components/widget/edit-mode-overlay/che-edit-mode-overlay.directive.ts +++ b/src/components/widget/edit-mode-overlay/che-edit-mode-overlay.directive.ts @@ -17,25 +17,67 @@ export interface ICheEditModeOverlayMessage { } export interface ICheEditModeOverlayButton { - action: (args?: any) => void; + /** + * Listeners to be called on button click. + */ + action: (...args: any[]) => void; + /** + * Set field to `true` to disable button. + */ disabled?: boolean; + /** + * Button name attribute value. + */ name?: string; + /** + * Button title + */ title?: string; } export interface ICheEditModeOverlayConfig { + /** + * Set field to `true` to show the overlay. + */ visible?: boolean; + /** + * Set field to `true` to disable all buttons. + */ disabled?: boolean; + /** + * A message to show. + */ message?: ICheEditModeOverlayMessage; + /** + * "Save" button. + */ saveButton?: ICheEditModeOverlayButton; + /** + * "Apply" button. + */ applyButton?: ICheEditModeOverlayButton; + /** + * "Cancel" button. + */ cancelButton?: ICheEditModeOverlayButton; + /** + * If `true` then user cannot leave the pages if there are unsaved changes. + */ + preventPageLeave?: boolean; + /** + * Listener to be called on `$locationChangeStart`. + */ + onChangesDiscard?: () => ng.IPromise; } export class CheEditModeOverlay implements ng.IDirective { restrict = 'E'; + bindToController = true; + controller = 'CheEditModeOverlayController'; + controllerAs = 'cheEditModeOverlayController'; + scope = { config: '=' }; @@ -44,37 +86,37 @@ export class CheEditModeOverlay implements ng.IDirective { return `
+ ng-if="cheEditModeOverlayController.config.visible">
- +
-
- + + name="{{cheEditModeOverlayController.config.saveButton.name || 'save-button'}}" + ng-disabled="cheEditModeOverlayController.config.disabled || cheEditModeOverlayController.config.saveButton.disabled" + ng-click="cheEditModeOverlayController.config.saveButton.action()">
-
- + + ng-disabled="cheEditModeOverlayController.config.disabled || cheEditModeOverlayController.config.applyButton.disabled" + ng-click="cheEditModeOverlayController.config.applyButton.action()">
-
- + + ng-disabled="cheEditModeOverlayController.config.cancelButton.disabled" + ng-click="cheEditModeOverlayController.config.cancelButton.action()">
`; diff --git a/src/components/widget/widget-config.ts b/src/components/widget/widget-config.ts index fa984b79c38..86a603d087e 100644 --- a/src/components/widget/widget-config.ts +++ b/src/components/widget/widget-config.ts @@ -91,6 +91,7 @@ import {CheEditorController} from './editor/che-editor.controller'; import {PagingButtons} from './paging-button/paging-button.directive'; import {CheRowToolbar} from './toolbar/che-row-toolbar.directive'; import {CheEditModeOverlay} from './edit-mode-overlay/che-edit-mode-overlay.directive'; +import { CheEditModeOverlayController } from './edit-mode-overlay/che-edit-mode-overlay.controller'; export class WidgetConfig { @@ -204,6 +205,7 @@ export class WidgetConfig { register.directive('cheTogglePopover', CheTogglePopover); register.directive('toggleButtonPopover', CheToggleButtonPopover); // edit overlay + register.controller('CheEditModeOverlayController', CheEditModeOverlayController); register.directive('cheEditModeOverlay', CheEditModeOverlay); } }