root/resilient#2 Change the User selection control to a 'on type' combo.

Also restricted the user list to only users that have NO association to
org yet
This commit is contained in:
Orlando M Guerreiro 2025-06-04 12:22:49 +01:00
parent 4890950952
commit 2aa75f0e13
8 changed files with 174 additions and 82 deletions

View file

@ -9,6 +9,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import com.oguerreiro.resilient.domain.User; import com.oguerreiro.resilient.domain.User;
@ -43,4 +44,13 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findOneWithAuthoritiesByEmailIgnoreCase(String email); Optional<User> findOneWithAuthoritiesByEmailIgnoreCase(String email);
Page<User> findAllByIdNotNullAndActivatedIsTrue(Pageable pageable); Page<User> findAllByIdNotNullAndActivatedIsTrue(Pageable pageable);
@Query("""
SELECT u
FROM User u
WHERE u.id IS NOT NULL
AND u.activated = TRUE
AND u.id NOT IN (SELECT o.user.id FROM Organization o WHERE o.user IS NOT NULL)
""")
Page<User> findActiveUsersNotInOrganization(Pageable pageable);
} }

View file

@ -274,6 +274,11 @@ public class UserService {
return userRepository.findAllByIdNotNullAndActivatedIsTrue(pageable).map(UserDTO::new); return userRepository.findAllByIdNotNullAndActivatedIsTrue(pageable).map(UserDTO::new);
} }
@Transactional(readOnly = true)
public Page<UserDTO> getAllPublicUsersForOrganization(Pageable pageable) {
return userRepository.findActiveUsersNotInOrganization(pageable).map(UserDTO::new);
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Optional<User> getUserWithAuthoritiesByLogin(String login) { public Optional<User> getUserWithAuthoritiesByLogin(String login) {
return userRepository.findOneWithAuthoritiesByLogin(login); return userRepository.findOneWithAuthoritiesByLogin(login);

View file

@ -1,69 +1,83 @@
package com.oguerreiro.resilient.service.dto; package com.oguerreiro.resilient.service.dto;
import com.oguerreiro.resilient.domain.User;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects; import java.util.Objects;
import com.oguerreiro.resilient.domain.User;
/** /**
* A DTO representing a user, with only the public attributes. * A DTO representing a user, with only the public attributes.
*/ */
public class UserDTO implements Serializable { public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private Long id; private Long id;
private String login; private String login;
public UserDTO() { private String email;
// Empty constructor needed for Jackson.
public UserDTO() {
// Empty constructor needed for Jackson.
}
public UserDTO(User user) {
this.id = user.getId();
this.login = user.getLogin();
this.email = user.getEmail();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
} }
public UserDTO(User user) { UserDTO userDTO = (UserDTO) o;
this.id = user.getId(); if (userDTO.getId() == null || getId() == null) {
// Customize it here if you need, or not, firstName/lastName/etc return false;
this.login = user.getLogin();
} }
public Long getId() { return Objects.equals(getId(), userDTO.getId()) && Objects.equals(getLogin(), userDTO.getLogin());
return id; }
}
public void setId(Long id) { // prettier-ignore
this.id = id; @Override
} public String toString() {
//@formatter:off
public String getLogin() { return "UserDTO{"
return login; + "id='" + id + '\''
} + ", login='" + login + '\''
+ ", email='" + email + '\''
public void setLogin(String login) { + "}";
this.login = login; //@formatter:on
} }
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UserDTO userDTO = (UserDTO) o;
if (userDTO.getId() == null || getId() == null) {
return false;
}
return Objects.equals(getId(), userDTO.getId()) && Objects.equals(getLogin(), userDTO.getLogin());
}
// prettier-ignore
@Override
public String toString() {
return "UserDTO{" +
"id='" + id + '\'' +
", login='" + login + '\'' +
"}";
}
} }

View file

@ -1,9 +1,9 @@
package com.oguerreiro.resilient.web.rest; package com.oguerreiro.resilient.web.rest;
import com.oguerreiro.resilient.service.UserService; import java.util.Arrays;
import com.oguerreiro.resilient.service.dto.UserDTO;
import java.util.*;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@ -12,45 +12,72 @@ import org.springframework.data.domain.Sort;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import com.oguerreiro.resilient.service.UserService;
import com.oguerreiro.resilient.service.dto.UserDTO;
import tech.jhipster.web.util.PaginationUtil; import tech.jhipster.web.util.PaginationUtil;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
public class PublicUserResource { public class PublicUserResource {
private static final List<String> ALLOWED_ORDERED_PROPERTIES = Collections.unmodifiableList( private static final List<String> ALLOWED_ORDERED_PROPERTIES = Collections.unmodifiableList(
Arrays.asList("id", "login", "firstName", "lastName", "email", "activated", "langKey") Arrays.asList("id", "login", "firstName", "lastName", "email", "activated", "langKey"));
);
private final Logger log = LoggerFactory.getLogger(PublicUserResource.class); private final Logger log = LoggerFactory.getLogger(PublicUserResource.class);
private final UserService userService; private final UserService userService;
public PublicUserResource(UserService userService) { public PublicUserResource(UserService userService) {
this.userService = userService; this.userService = userService;
}
/**
* {@code GET /users} : get all users with only public information - calling this method is allowed for anyone.
*
* @param pageable the pagination information.
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body all users.
*/
@GetMapping("/users")
public ResponseEntity<List<UserDTO>> getAllPublicUsers(
@org.springdoc.core.annotations.ParameterObject Pageable pageable) {
log.debug("REST request to get all public User names");
if (!onlyContainsAllowedProperties(pageable)) {
return ResponseEntity.badRequest().build();
} }
/** final Page<UserDTO> page = userService.getAllPublicUsers(pageable);
* {@code GET /users} : get all users with only public information - calling this method is allowed for anyone. HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(),
* page);
* @param pageable the pagination information. return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK);
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body all users. }
*/
@GetMapping("/users")
public ResponseEntity<List<UserDTO>> getAllPublicUsers(@org.springdoc.core.annotations.ParameterObject Pageable pageable) {
log.debug("REST request to get all public User names");
if (!onlyContainsAllowedProperties(pageable)) {
return ResponseEntity.badRequest().build();
}
final Page<UserDTO> page = userService.getAllPublicUsers(pageable); /**
HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); * {@code GET /users} : get all users with only public information - calling this method is allowed for anyone.
return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); *
* @param pageable the pagination information.
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body all users.
*/
@GetMapping("/users/for/organization")
public ResponseEntity<List<UserDTO>> getAllPublicUsersForOrganization(
@org.springdoc.core.annotations.ParameterObject Pageable pageable) {
log.debug("REST request to get all public User names, restricted to users not yet associated in the organization");
if (!onlyContainsAllowedProperties(pageable)) {
return ResponseEntity.badRequest().build();
} }
private boolean onlyContainsAllowedProperties(Pageable pageable) { final Page<UserDTO> page = userService.getAllPublicUsersForOrganization(pageable);
return pageable.getSort().stream().map(Sort.Order::getProperty).allMatch(ALLOWED_ORDERED_PROPERTIES::contains); HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(),
} page);
return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK);
}
private boolean onlyContainsAllowedProperties(Pageable pageable) {
return pageable.getSort().stream().map(Sort.Order::getProperty).allMatch(ALLOWED_ORDERED_PROPERTIES::contains);
}
} }

View file

@ -186,12 +186,36 @@
<!-- User property applies only to OrganizationNature.PERSON --> <!-- User property applies only to OrganizationNature.PERSON -->
<div class="mb-3" *ngIf="editForm.get('organizationType')?.value?.nature === organizationNature.PERSON"> <div class="mb-3" *ngIf="editForm.get('organizationType')?.value?.nature === organizationNature.PERSON">
<label class="form-label" for="field_user" jhiTranslate="resilientApp.organization.user">User</label> <label class="form-label" for="field_user" jhiTranslate="resilientApp.organization.user">User</label>
<select class="form-control" id="field_user" data-cy="user" name="user" formControlName="user" [compareWith]="compareUser"> <!--
<select
class="form-control"
id="field_user"
data-cy="user"
name="user"
formControlName="user"
[compareWith]="compareUser">
<option [ngValue]="null"></option> <option [ngValue]="null"></option>
@for (userOption of usersSharedCollection; track $index) { @for (userOption of usersSharedCollection; track $index) {
<option [ngValue]="userOption">{{ userOption.login }}</option> <option [ngValue]="userOption">{{ userOption.login }}</option>
} }
</select> </select>
-->
<ng-select
class="form-control drop-down-panel"
formControlName="user"
[items]="usersSharedCollection"
[compareWith]="compareUser"
[clearable]="true"
[searchFn]="userSearchFn"
>
<ng-template ng-label-tmp let-item="item">
{{ item.login }} ( {{ item.email }} )
</ng-template>
<ng-template ng-option-tmp let-item="item">
{{ item.login }} ( {{ item.email }} )
</ng-template>
</ng-select>
</div> </div>
</div> </div>

View file

@ -6,6 +6,7 @@ import { finalize, map } from 'rxjs/operators';
import SharedModule from 'app/shared/shared.module'; import SharedModule from 'app/shared/shared.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select';
import { AlertError } from 'app/shared/alert/alert-error.model'; import { AlertError } from 'app/shared/alert/alert-error.model';
import { EventManager, EventWithContent } from 'app/core/util/event-manager.service'; import { EventManager, EventWithContent } from 'app/core/util/event-manager.service';
@ -23,7 +24,7 @@ import { OrganizationNature } from 'app/entities/enumerations/organization-natur
standalone: true, standalone: true,
selector: 'jhi-organization-update', selector: 'jhi-organization-update',
templateUrl: './organization-update.component.html', templateUrl: './organization-update.component.html',
imports: [SharedModule, FormsModule, ReactiveFormsModule], imports: [SharedModule, FormsModule, ReactiveFormsModule, NgSelectModule],
}) })
export class OrganizationUpdateComponent implements OnInit { export class OrganizationUpdateComponent implements OnInit {
// Properties with defaults by router // Properties with defaults by router
@ -181,7 +182,7 @@ export class OrganizationUpdateComponent implements OnInit {
protected loadRelationshipsOptionsUser(): void { protected loadRelationshipsOptionsUser(): void {
this.userService this.userService
.query() .queryForOrganization()
.pipe(map((res: HttpResponse<IUser[]>) => res.body ?? [])) .pipe(map((res: HttpResponse<IUser[]>) => res.body ?? []))
.pipe(map((users: IUser[]) => this.userService.addUserToCollectionIfMissing<IUser>(users, this.organization?.user))) .pipe(map((users: IUser[]) => this.userService.addUserToCollectionIfMissing<IUser>(users, this.organization?.user)))
.subscribe((users: IUser[]) => (this.usersSharedCollection = users)); .subscribe((users: IUser[]) => (this.usersSharedCollection = users));
@ -227,4 +228,9 @@ export class OrganizationUpdateComponent implements OnInit {
} }
}); });
} }
userSearchFn = (term: string, item: any) => {
term = term.toLowerCase();
return (item.login + "( " + item.email + " )").toLowerCase().includes(term);
};
} }

View file

@ -26,6 +26,11 @@ export class UserService {
return this.http.get<IUser[]>(this.resourceUrl, { params: options, observe: 'response' }); return this.http.get<IUser[]>(this.resourceUrl, { params: options, observe: 'response' });
} }
queryForOrganization(req?: any): Observable<EntityArrayResponseType> {
const options = createRequestOption(req);
return this.http.get<IUser[]>(`${this.resourceUrl}/for/organization`, { params: options, observe: 'response' });
}
getUserIdentifier(user: Pick<IUser, 'id'>): number { getUserIdentifier(user: Pick<IUser, 'id'>): number {
return user.id; return user.id;
} }

View file

@ -1,4 +1,5 @@
export interface IUser { export interface IUser {
id: number; id: number;
login?: string | null; login?: string | null;
email?: string | null;
} }