diff --git a/src/app/modules/project/add/main/wizard/wizard.component.ts b/src/app/modules/project/add/main/wizard/wizard.component.ts
index f75e82c4..7b658792 100644
--- a/src/app/modules/project/add/main/wizard/wizard.component.ts
+++ b/src/app/modules/project/add/main/wizard/wizard.component.ts
@@ -17,6 +17,7 @@
import { LocationStrategy } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
+import { BsModalRef } from 'ngx-bootstrap/modal';
import { Observable } from 'rxjs';
import { WizardPage } from 'src/app/models/domain/wizard-page';
import { AlertConfig } from 'src/app/models/internal/alert-config';
@@ -46,7 +47,8 @@ export class WizardComponent implements OnInit {
private projectService: ProjectService,
private alertService: AlertService,
private location: LocationStrategy,
- private authService: AuthService) {
+ private authService: AuthService,
+ public bsModalRef: BsModalRef) {
// check if back or forward button is pressed and prevent it.
this.registerNavigationListener();
}
diff --git a/src/app/modules/project/add/main/wizard/wizardPages/default/project-name/project-name.component.html b/src/app/modules/project/add/main/wizard/wizardPages/default/project-name/project-name.component.html
index 8c813395..a00384f3 100644
--- a/src/app/modules/project/add/main/wizard/wizardPages/default/project-name/project-name.component.html
+++ b/src/app/modules/project/add/main/wizard/wizardPages/default/project-name/project-name.component.html
@@ -30,6 +30,9 @@
Previous
+
+ Project name should be less than {{ projectName.errors.maxlength.requiredLength }} characters
+
diff --git a/src/app/modules/project/add/main/wizard/wizardPages/default/project-name/project-name.component.ts b/src/app/modules/project/add/main/wizard/wizardPages/default/project-name/project-name.component.ts
index 4ebb2456..66d3cc71 100644
--- a/src/app/modules/project/add/main/wizard/wizardPages/default/project-name/project-name.component.ts
+++ b/src/app/modules/project/add/main/wizard/wizardPages/default/project-name/project-name.component.ts
@@ -15,7 +15,7 @@
* If not, see https://www.gnu.org/licenses/lgpl-3.0.txt
*/
import { Component, OnInit } from '@angular/core';
-import { FormControl } from '@angular/forms';
+import { FormControl, Validators } from '@angular/forms';
import { ProjectAdd } from 'src/app/models/resources/project-add';
import { WizardStepBaseComponent } from 'src/app/modules/project/add/main/wizard/wizardPages/wizard-step-base/wizard-step-base.component';
import { WizardService } from 'src/app/services/wizard.service';
@@ -30,7 +30,7 @@ export class ProjectNameComponent extends WizardStepBaseComponent implements OnI
/**
* Form fields
*/
- public projectName = new FormControl('');
+ public projectName = new FormControl('', Validators.maxLength(75));
/**
* Remembers if project-name is the first step in the current wizard
*/
diff --git a/src/app/modules/project/details/bottom-drawer/settings-dropdown/settings-dropdown.component.html b/src/app/modules/project/details/bottom-drawer/settings-dropdown/settings-dropdown.component.html
index 65e1ada2..3864a94d 100644
--- a/src/app/modules/project/details/bottom-drawer/settings-dropdown/settings-dropdown.component.html
+++ b/src/app/modules/project/details/bottom-drawer/settings-dropdown/settings-dropdown.component.html
@@ -12,5 +12,8 @@
Delete project
+
+ Transfer ownership
+
diff --git a/src/app/modules/project/details/bottom-drawer/settings-dropdown/settings-dropdown.component.ts b/src/app/modules/project/details/bottom-drawer/settings-dropdown/settings-dropdown.component.ts
index 994ab4e8..cb24b3a1 100644
--- a/src/app/modules/project/details/bottom-drawer/settings-dropdown/settings-dropdown.component.ts
+++ b/src/app/modules/project/details/bottom-drawer/settings-dropdown/settings-dropdown.component.ts
@@ -4,6 +4,9 @@ import { BsModalService, ModalOptions } from 'ngx-bootstrap/modal';
import { EMPTY } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { ModalDeleteGenericComponent } from 'src/app/components/modals/modal-delete-generic/modal-delete-generic.component';
+import { ModalInformationGenericComponent } from 'src/app/components/modals/modal-information-generic/modal-information-generic.component';
+import { ModalPotentialNewOwnerUserEmailConfirmationComponent } from 'src/app/components/modals/modal-potential-new-owner-user-email-confirmation/modal-potential-new-owner-user-email-confirmation.component';
+import { ModalPotentialNewOwnerUserEmailComponent } from 'src/app/components/modals/modal-potential-new-owner-user-email/modal-potential-new-owner-user-email.component';
import { Highlight } from 'src/app/models/domain/highlight';
import { Project } from 'src/app/models/domain/project';
import { scopes } from 'src/app/models/domain/scopes';
@@ -34,6 +37,11 @@ export class SettingsDropdownComponent implements OnInit {
public displayEditButton = false;
public displayDeleteProjectButton = false;
public displayHighlightButton = false;
+ public displayTransferProjectOwnershipButton = false;
+
+ public potentialNewOwnerUserEmail = '';
+ private transferGuid: string = null;
+
constructor(private projectService: ProjectService,
private authService: AuthService,
@@ -170,6 +178,106 @@ export class SettingsDropdownComponent implements OnInit {
});
}
+ /**
+ * Method triggered on transfer ownership button pressed.
+ * Opens modal depending on if the project has a existing transfer request.
+ */
+ public onClickTransferOwnership() {
+ this.projectService.checkProjectHasTransferRequest(this.project.id).subscribe(guid => {
+ this.transferGuid = guid;
+ this.showTransferRequestStatusModal();
+ }, () => {
+ this.showPotentialNewOwnerUserEmailModal();
+ });
+ }
+
+ /**
+ * Method for showing the potential new owner user email modal.
+ */
+ private showPotentialNewOwnerUserEmailModal() {
+ const modalRefEmail = this.modalService.show(ModalPotentialNewOwnerUserEmailComponent);
+ modalRefEmail.content.potentialNewOwnerUserEmailEvent.subscribe((potentialNewOwnerUserEmail: string) => {
+ this.potentialNewOwnerUserEmail = potentialNewOwnerUserEmail;
+ this.showPotentialNewOwnerUserEmailConfirmationModal();
+ });
+ }
+
+ /**
+ * Method for showing the project ownership transfer request status with
+ * possibility to cancel this request.
+ */
+ private showTransferRequestStatusModal() {
+ const modalOptions: ModalOptions = {
+ initialState: {
+ titleText: 'Transfer ownership request',
+ mainText: `Your transfer ownership request for this project is waiting for acceptence or denial.`,
+ ctaButtonText: 'Cancel request',
+ secondaryButtonText: 'Go back'
+ }
+ };
+ const informationRefModal = this.modalService.show(ModalInformationGenericComponent, modalOptions);
+ informationRefModal.content.cta.subscribe(() => {
+ this.showDeleteTransferRequestModal();
+ });
+ }
+
+ /**
+ * Method for showing the potential new owner user email confirmation modal.
+ */
+ private showDeleteTransferRequestModal() {
+ const modalOptions: ModalOptions = {
+ initialState: {
+ titleText: 'Cancel transfer request',
+ mainText: `Are you sure you want to cancel the transfer request for this project, ${this.project.name}?`,
+ }
+ };
+ const deleteRefModal = this.modalService.show(ModalDeleteGenericComponent, modalOptions);
+ deleteRefModal.content.remove.subscribe(() => {
+ this.projectService.deleteTransferRequest(this.transferGuid).subscribe(() => {
+ const alertConfig: AlertConfig = {
+ type: AlertType.success,
+ preMessage: '',
+ mainMessage: 'Transfer ownership request canceled',
+ dismissible: true,
+ autoDismiss: true,
+ timeout: 10000
+ };
+ this.alertService.pushAlert(alertConfig);
+ });
+ });
+ }
+
+ /**
+ * Method for asking user to confirm filled in potential new owner user email.
+ */
+ private showPotentialNewOwnerUserEmailConfirmationModal() {
+ const email = this.potentialNewOwnerUserEmail;
+ const modalRefEmailConfirm = this.modalService.show(ModalPotentialNewOwnerUserEmailConfirmationComponent, {initialState: {email}});
+ modalRefEmailConfirm.content.didConfirmEvent.subscribe(() => {
+ this.projectService.initiateTransferProjectOwnership(this.project.id, this.potentialNewOwnerUserEmail).subscribe(() => {
+ const alertConfig: AlertConfig = {
+ type: AlertType.success,
+ preMessage: '',
+ mainMessage: 'Transfer ownership request confirmation has been sent to your email inbox',
+ dismissible: true,
+ autoDismiss: true,
+ timeout: 10000
+ };
+ this.alertService.pushAlert(alertConfig);
+ }, () => {
+ const alertConfig: AlertConfig = {
+ type: AlertType.danger,
+ preMessage: '',
+ mainMessage: 'Email not sent, make sure you fill in the right email',
+ dismissible: true,
+ autoDismiss: true,
+ timeout: 10000
+ };
+ this.alertService.pushAlert(alertConfig);
+ });
+ });
+ }
+
/**
* Method to display the edit project button based on the current user and the project user.
* If the user either has the ProjectWrite scope or is the creator of the project
@@ -178,12 +286,14 @@ export class SettingsDropdownComponent implements OnInit {
if (this.currentUser == null || this.project == null || this.project.user == null) {
this.displayEditButton = false;
this.displayDeleteProjectButton = false;
+ this.displayTransferProjectOwnershipButton = false;
return;
}
if (this.project.user.id === this.currentUser.id ||
this.authService.currentBackendUserHasScope(scopes.AdminProjectWrite)) {
this.displayEditButton = true;
this.displayDeleteProjectButton = true;
+ this.displayTransferProjectOwnershipButton = true;
}
}
diff --git a/src/app/modules/project/details/edit/edit.component.html b/src/app/modules/project/details/edit/edit.component.html
index dea76bb2..632ee87e 100644
--- a/src/app/modules/project/details/edit/edit.component.html
+++ b/src/app/modules/project/details/edit/edit.component.html
@@ -149,7 +149,7 @@
Collaborator role
-
diff --git a/src/app/modules/project/details/edit/edit.component.scss b/src/app/modules/project/details/edit/edit.component.scss
index fe15e04d..463d1b0c 100644
--- a/src/app/modules/project/details/edit/edit.component.scss
+++ b/src/app/modules/project/details/edit/edit.component.scss
@@ -72,6 +72,15 @@
opacity: 1;
}
}
+
+ @media only screen and (max-width: 600px) {
+ .overlay{
+ opacity: 1;
+ em {
+ opacity: .8;
+ }
+ }
+ }
}
.icon-remove-btn {
grid-column: 2;
@@ -339,6 +348,17 @@
opacity: 1;
}
}
+
+ @media only screen and (max-width: 600px) {
+ .overlay {
+ &.trash{
+ opacity: 1;
+ em {
+ opacity: .8;
+ }
+ }
+ }
+ }
}
img {
diff --git a/src/app/modules/project/modal-highlight-form/modal-highlight-form.component.ts b/src/app/modules/project/modal-highlight-form/modal-highlight-form.component.ts
index bf2c1d22..495c59d2 100644
--- a/src/app/modules/project/modal-highlight-form/modal-highlight-form.component.ts
+++ b/src/app/modules/project/modal-highlight-form/modal-highlight-form.component.ts
@@ -52,7 +52,6 @@ export class ModalHighlightFormComponent implements OnInit, AfterViewInit {
@ViewChild(FileUploaderComponent) fileUploader: FileUploaderComponent;
-
public highlightProjectForm: FormGroup;
public dateFieldsEnabled = true;
public validationErrorMessage: string = null;
diff --git a/src/app/modules/project/overview/filter-menu/filter-menu.component.ts b/src/app/modules/project/overview/filter-menu/filter-menu.component.ts
index d8ba1b8c..aac26522 100644
--- a/src/app/modules/project/overview/filter-menu/filter-menu.component.ts
+++ b/src/app/modules/project/overview/filter-menu/filter-menu.component.ts
@@ -165,11 +165,12 @@ export class FilterMenuComponent implements OnInit {
* Method that retrieves the value that has changed from the pagination dropdown in the accordion,
* and based on that value retrieves the paginated projects with the right parameters.
*/
- public onPaginationChange() {
+ public onPaginationChange(page: number = this.currentPage) {
this.amountOfProjectsOnSinglePage = this.paginationOptionControl.value.amountOnPage;
if (this.amountOfProjectsOnSinglePage === this.paginationResponse.totalCount) {
this.currentPage = 1;
}
+ this.currentPage = page;
this.updateQueryParams();
this.onInternalQueryChange();
}
@@ -202,7 +203,6 @@ export class FilterMenuComponent implements OnInit {
.filter(value => value)
};
-
if (internalSearchQuery.query == null) {
// No search query provided use projectService.
this.paginationService
diff --git a/src/app/modules/project/overview/overview.component.ts b/src/app/modules/project/overview/overview.component.ts
index 9f18710d..bd0de382 100644
--- a/src/app/modules/project/overview/overview.component.ts
+++ b/src/app/modules/project/overview/overview.component.ts
@@ -119,8 +119,7 @@ export class OverviewComponent implements OnInit, AfterViewInit {
*/
public pageChanged(page: number): void {
this.currentPage = page;
-
- this.filterMenu.onPaginationChange();
+ this.filterMenu.onPaginationChange(page);
}
public filteredProjectsChanged(result): void {
diff --git a/src/app/modules/project/project-routing.module.ts b/src/app/modules/project/project-routing.module.ts
index b6c92cad..7f6baecf 100644
--- a/src/app/modules/project/project-routing.module.ts
+++ b/src/app/modules/project/project-routing.module.ts
@@ -17,6 +17,7 @@
import { EditComponent } from './details/edit/edit.component';
import { EmbedComponent } from './embed/embed.component';
import { OverviewComponent } from './overview/overview.component';
+import { TransferOwnershipComponent } from './transfer-ownership/transfer-ownership.component';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
@@ -26,6 +27,7 @@ const routes: Routes = [
{path: 'details/:id', component: OverviewComponent},
{path: 'edit/:id', component: EditComponent},
{path: 'embed/:id', component: EmbedComponent},
+ {path: 'transferownership/:transferGuid/:isOwnerMail/:acceptedRequest', component: TransferOwnershipComponent},
{path: 'add', loadChildren: () => import('./add/add.module').then((m) => m.AddModule)},
];
diff --git a/src/app/modules/project/transfer-ownership/transfer-ownership.component.html b/src/app/modules/project/transfer-ownership/transfer-ownership.component.html
new file mode 100644
index 00000000..34efa3db
--- /dev/null
+++ b/src/app/modules/project/transfer-ownership/transfer-ownership.component.html
@@ -0,0 +1,14 @@
+
+
+
+
Loading...
+
+
+ Success
+ Error 400 Bad Request
+ Error 404 Not Found
+ {{reply}}
+
+
Go back to home
+
+
diff --git a/src/app/modules/project/transfer-ownership/transfer-ownership.component.scss b/src/app/modules/project/transfer-ownership/transfer-ownership.component.scss
new file mode 100644
index 00000000..b234efda
--- /dev/null
+++ b/src/app/modules/project/transfer-ownership/transfer-ownership.component.scss
@@ -0,0 +1,17 @@
+@import "assets/styles/variables";
+
+.container-transfer-ownership {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ margin-top: 50px;
+
+ button{
+ margin-top: 30px;
+ width: 250px;
+ }
+
+ a {
+ color: $accent-color-red-primary;
+ }
+}
diff --git a/src/app/modules/project/transfer-ownership/transfer-ownership.component.ts b/src/app/modules/project/transfer-ownership/transfer-ownership.component.ts
new file mode 100644
index 00000000..9c95b5d1
--- /dev/null
+++ b/src/app/modules/project/transfer-ownership/transfer-ownership.component.ts
@@ -0,0 +1,41 @@
+import { HttpErrorResponse } from '@angular/common/http';
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { catchError } from 'rxjs/operators';
+import { ProjectService } from 'src/app/services/project.service';
+
+@Component({
+ selector: 'app-transfer-ownership',
+ templateUrl: './transfer-ownership.component.html',
+ styleUrls: ['./transfer-ownership.component.scss']
+})
+export class TransferOwnershipComponent implements OnInit {
+
+ private transferGuid;
+ private isOwnerMail;
+ private acceptedRequest;
+
+ public reply = '';
+ public replyStatus: number;
+ public isLoading = true;
+
+ constructor(
+ private projectService: ProjectService,
+ private route: ActivatedRoute,
+ ) {
+ this.transferGuid = this.route.snapshot.url[1];
+ this.isOwnerMail = this.route.snapshot.url[2];
+ this.acceptedRequest = this.route.snapshot.url[3];
+ }
+
+ ngOnInit(): void {
+ this.projectService.processTransferProjectOwnership(this.transferGuid, this.isOwnerMail, this.acceptedRequest).subscribe(response => {
+ this.reply = response.body;
+ this.replyStatus = response.status;
+ this.isLoading = false;
+ }, (error: HttpErrorResponse) => {
+ this.reply = error.message;
+ this.replyStatus = error.status;
+ });
+ }
+}
diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts
index bbcc3396..1ccec69b 100644
--- a/src/app/services/auth.service.ts
+++ b/src/app/services/auth.service.ts
@@ -44,7 +44,7 @@ export class AuthService {
private backenduser: BackendUser | null;
constructor(
- private userService: UserService
+ private userService: UserService
) {
this.manager = new UserManager(getClientSettings());
@@ -69,7 +69,7 @@ export class AuthService {
*/
public login(providerSchema?: string): Promise {
if (providerSchema != null) {
- this.manager.settings.extraQueryParams = { 'provider': providerSchema };
+ this.manager.settings.extraQueryParams = {'provider': providerSchema};
}
return this.manager.signinRedirect();
}
@@ -130,6 +130,31 @@ export class AuthService {
return this.user != null && !this.user.expired;
}
+ /**
+ * Checks if the users token is expired.
+ * @returns true if expired.
+ */
+ public isUserExpired(): boolean {
+ return this.user.expired;
+ }
+
+ /**
+ * Silent login, for use of the refresh token
+ */
+ public silentLogin(): void {
+ this.manager.signinSilent().then(() => {
+ this.manager.getUser().then((user) => {
+ this.user = user;
+ if (this.isAuthenticated()) {
+ this.getBackendUser().then((backendUser) => {
+ this.backenduser = backendUser;
+ this.$user.next(backendUser);
+ });
+ }
+ }).catch(err => console.log(err));
+ });
+ }
+
/**
* Gets the authorization header value.
*/
diff --git a/src/app/services/project.service.ts b/src/app/services/project.service.ts
index 9a322412..fca5ff3e 100644
--- a/src/app/services/project.service.ts
+++ b/src/app/services/project.service.ts
@@ -38,7 +38,7 @@ export class ProjectService extends HttpBaseService = new EventEmitter();
- get(id: number): Observable {
+ public get(id: number): Observable {
return super.get(id)
.pipe(
map(project => {
@@ -58,7 +58,25 @@ export class ProjectService extends HttpBaseService {
+ public initiateTransferProjectOwnership(projectId: number, potentialNewOwnerUserEmail: string): Observable {
+ return this.http.post(`${this.url}/transfer/${projectId}`, '',
+ {responseType: 'text', params: {potentialNewOwnerUserEmail: potentialNewOwnerUserEmail}}
+ );
+ }
+
+ public processTransferProjectOwnership(transferGuid: string, isOwnerMail: boolean, acceptedRequest: boolean): Observable {
+ return this.http.get(`${this.url}/transfer/process/${transferGuid}/${isOwnerMail}/${acceptedRequest}`, {responseType: 'text', observe: 'response'});
+ }
+
+ public checkProjectHasTransferRequest(projectId: number): Observable {
+ return this.http.get(`${this.url}/transfer/check/${projectId}`);
+ }
+
+ public deleteTransferRequest(transferGuid: string): Observable {
+ return this.http.delete(`${this.url}/transfer/${transferGuid}`, {responseType: 'text'});
+ }
+
+ private addLikes(project: any): Promise {
return this.authService.getBackendUser()
.then(currentUser => {
project.likeCount = project.likes?.length ? project.likes.length : 0;
diff --git a/src/styles.scss b/src/styles.scss
index 6d66d152..178abe1e 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -68,7 +68,8 @@ body {
display: flex;
align-items: center;
justify-content: center;
-
+ animation-timing-function: ease-in-out;
+ -webkit-animation-timing-function: ease-in-out;
h2 {
font-size: 3em;
color: $accent-color-red-primary !important;