Skip to content

Commit

Permalink
Workflow management (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
haiphucnguyen authored Dec 8, 2024
1 parent ba17c93 commit 2146de4
Show file tree
Hide file tree
Showing 19 changed files with 254 additions and 205 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.List;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
Expand Down Expand Up @@ -48,11 +48,11 @@ public class Workflow {

@EqualsAndHashCode.Exclude
@OneToMany(mappedBy = "workflow", cascade = CascadeType.ALL, orphanRemoval = true)
private List<WorkflowState> states;
private Set<WorkflowState> states;

@EqualsAndHashCode.Exclude
@OneToMany(mappedBy = "workflow", cascade = CascadeType.ALL, orphanRemoval = true)
private List<WorkflowTransition> transitions;
private Set<WorkflowTransition> transitions;

@Column(
name = "level1_escalation_timeout",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import io.flexwork.modules.teams.domain.Workflow;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface WorkflowRepository extends JpaRepository<Workflow, Long> {
public interface WorkflowRepository
extends JpaRepository<Workflow, Long>, JpaSpecificationExecutor<Workflow> {

@Query(
"""
Expand All @@ -34,4 +37,8 @@ public interface WorkflowRepository extends JpaRepository<Workflow, Long> {
""")
Optional<Integer> findEscalationTimeoutByLevel(
@Param("workflowId") Long workflowId, @Param("level") int level);

@EntityGraph(attributePaths = {"states", "transitions", "owner"})
@Query("SELECT w FROM Workflow w WHERE w.id = :workflowId")
Optional<Workflow> findWithDetailsById(@Param("workflowId") Long workflowId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public TeamRequestDTO createTeamRequest(TeamRequestDTO teamRequestDTO) {

TeamRequest teamRequest = teamRequestMapper.toEntity(teamRequestDTO);
teamRequest = teamRequestRepository.save(teamRequest);

// Clear the persistence context to force a reload
entityManager.clear();

Expand Down Expand Up @@ -172,8 +173,6 @@ public TeamRequestDTO updateTeamRequest(TeamRequestDTO teamRequestDTO) {
TeamRequestDTO savedTeamRequest =
teamRequestMapper.toDto(teamRequestRepository.save(existingTeamRequest));

entityManager.clear();

eventPublisher.publishEvent(
new AuditLogUpdateEvent(this, previousTeamRequest, teamRequestDTO));

Expand Down Expand Up @@ -235,7 +234,7 @@ private static boolean containsTeamIdFilterInGroup(GroupFilter groupFilter) {
// Recursively check nested groups for "team.id"
if (groupFilter.getGroups() != null) {
return groupFilter.getGroups().stream()
.anyMatch(nestedGroup -> containsTeamIdFilterInGroup(nestedGroup));
.anyMatch(TeamRequestService::containsTeamIdFilterInGroup);
}

return false;
Expand Down Expand Up @@ -317,7 +316,7 @@ public Long countOverdueTickets(Long teamId) {
return teamRequestRepository.countOverdueTicketsByTeamId(teamId, Completed);
}

public List<TicketActionCountByDateDTO> getTicketCreationTimeseries(Long teamId, int days) {
public List<TicketActionCountByDateDTO> getTicketCreationTimeSeries(Long teamId, int days) {
if (days <= 0) {
days = 7; // Default to 7 days
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
package io.flexwork.modules.teams.service;

import static io.flexwork.query.QueryUtils.createSpecification;

import io.flexwork.modules.teams.domain.Workflow;
import io.flexwork.modules.teams.domain.WorkflowState;
import io.flexwork.modules.teams.domain.WorkflowTransition;
import io.flexwork.modules.teams.repository.WorkflowRepository;
import io.flexwork.modules.teams.repository.WorkflowStateRepository;
import io.flexwork.modules.teams.repository.WorkflowTransitionRepository;
import io.flexwork.modules.teams.service.dto.WorkflowDTO;
import io.flexwork.modules.teams.service.dto.WorkflowDetailedDTO;
import io.flexwork.modules.teams.service.mapper.WorkflowMapper;
import io.flexwork.modules.teams.service.mapper.WorkflowStateMapper;
import io.flexwork.modules.teams.service.mapper.WorkflowTransitionMapper;
import io.flexwork.query.QueryDTO;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -14,21 +28,34 @@ public class WorkflowService {

private final WorkflowRepository workflowRepository;

private final WorkflowStateRepository workflowStateRepository;

private final WorkflowTransitionRepository workflowTransitionRepository;

private final WorkflowMapper workflowMapper;

public WorkflowService(WorkflowRepository workflowRepository, WorkflowMapper workflowMapper) {
private final WorkflowStateMapper workflowStateMapper;

private final WorkflowTransitionMapper workflowTransitionMapper;

public WorkflowService(
WorkflowRepository workflowRepository,
WorkflowStateRepository workflowStateRepository,
WorkflowTransitionRepository workflowTransitionRepository,
WorkflowMapper workflowMapper,
WorkflowStateMapper workflowStateMapper,
WorkflowTransitionMapper workflowTransitionMapper) {
this.workflowRepository = workflowRepository;
this.workflowStateRepository = workflowStateRepository;
this.workflowTransitionRepository = workflowTransitionRepository;
this.workflowMapper = workflowMapper;
this.workflowStateMapper = workflowStateMapper;
this.workflowTransitionMapper = workflowTransitionMapper;
}

@Transactional
public Workflow createWorkflow(Workflow workflow) {
return workflowRepository.save(workflow);
}

@Transactional(readOnly = true)
public List<Workflow> getAllWorkflows() {
return workflowRepository.findAll();
public WorkflowDTO createWorkflow(Workflow workflow) {
return workflowMapper.toDto(workflowRepository.save(workflow));
}

@Transactional(readOnly = true)
Expand All @@ -37,14 +64,13 @@ public Optional<Workflow> getWorkflowById(Long id) {
}

@Transactional
public Workflow updateWorkflow(Long id, Workflow updatedWorkflow) {
public WorkflowDTO updateWorkflow(Long id, WorkflowDTO updatedWorkflow) {
return workflowRepository
.findById(id)
.map(
existingWorkflow -> {
existingWorkflow.setName(updatedWorkflow.getName());
existingWorkflow.setDescription(updatedWorkflow.getDescription());
return workflowRepository.save(existingWorkflow);
workflowMapper.updateEntity(updatedWorkflow, existingWorkflow);
return workflowMapper.toDto(workflowRepository.save(existingWorkflow));
})
.orElseThrow(
() -> new IllegalArgumentException("Workflow not found with id: " + id));
Expand All @@ -70,4 +96,69 @@ public List<WorkflowDTO> getWorkflowsForTeam(Long teamId) {
.map(workflowMapper::toDto)
.toList();
}

public Optional<WorkflowDetailedDTO> getWorkflowDetail(Long workflowId) {
return workflowRepository
.findWithDetailsById(workflowId)
.map(workflowMapper::toDetailedDto);
}

@Transactional(readOnly = true)
public Page<WorkflowDTO> findWorkflows(Optional<QueryDTO> queryDTO, Pageable pageable) {
Specification<Workflow> spec = createSpecification(queryDTO);
return workflowRepository.findAll(spec, pageable).map(workflowMapper::toDto);
}

@Transactional
public WorkflowDetailedDTO saveWorkflow(WorkflowDetailedDTO dto) {
Workflow workflow = workflowMapper.toEntity(dto);
Workflow savedWorkflow = workflowRepository.save(workflow);

// Save states
List<WorkflowState> states =
dto.getStates().stream()
.map(
stateDto -> {
WorkflowState state = workflowStateMapper.toEntity(stateDto);
state.setWorkflow(savedWorkflow);
return state;
})
.collect(Collectors.toList());
workflowStateRepository.saveAll(states);

// Save transitions
List<WorkflowTransition> transitions =
dto.getTransitions().stream()
.map(
transitionDto -> {
WorkflowTransition transition =
workflowTransitionMapper.toEntity(transitionDto);
transition.setWorkflow(savedWorkflow);
transition.setSourceState(
states.stream()
.filter(
state ->
state.getId()
.equals(
transitionDto
.getSourceStateId()))
.findFirst()
.orElse(null));
transition.setTargetState(
states.stream()
.filter(
state ->
state.getId()
.equals(
transitionDto
.getTargetStateId()))
.findFirst()
.orElse(null));
return transition;
})
.collect(Collectors.toList());
workflowTransitionRepository.saveAll(transitions);

return workflowMapper.toDetailedDto(workflow);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.flexwork.modules.teams.service.dto;

import io.flexwork.modules.teams.domain.WorkflowVisibility;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
Expand All @@ -15,11 +16,13 @@ public class WorkflowDTO {

private String name;

private String description;

private String requestName;

private String description;
private Long ownerId; // ID of the owning team; null for global workflows

boolean isGlobal;
private WorkflowVisibility visibility;

private Integer level1EscalationTimeout;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.flexwork.modules.teams.service.dto;

import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class WorkflowDetailedDTO extends WorkflowDTO {

private String
ownerName; // return the team name that own this workflow, if workflow is global then
// its value is null

private List<WorkflowStateDTO> states;

private List<WorkflowTransitionDTO> transitions;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.flexwork.modules.teams.service.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class WorkflowTransitionDTO {
private Long id;
private Long workflowId; // ID of the associated Workflow
private Long sourceStateId; // ID of the source state
private Long targetStateId; // ID of the target state
private String eventName; // Name of the triggering event
private Long slaDuration; // SLA duration for the transition (nullable)
private boolean escalateOnViolation; // Whether to escalate on SLA violation
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
package io.flexwork.modules.teams.service.mapper;

import io.flexwork.modules.teams.domain.Team;
import io.flexwork.modules.teams.domain.Workflow;
import io.flexwork.modules.teams.service.dto.WorkflowDTO;
import io.flexwork.modules.teams.service.dto.WorkflowDetailedDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.MappingTarget;

@Mapper(componentModel = "spring")
@Mapper(
componentModel = "spring",
uses = {WorkflowStateMapper.class, WorkflowTransitionMapper.class})
public interface WorkflowMapper {

@Mapping(source = "owner", target = "isGlobal", qualifiedByName = "mapIsGlobal")
@Mapping(source = "owner.id", target = "ownerId")
WorkflowDTO toDto(Workflow workflow);

Workflow toEntity(WorkflowDTO workflowDTO);

@Named("mapIsGlobal")
default boolean mapIsGlobal(Team owner) {
return owner == null; // If owner is null, it's a global workflow
}
@Mapping(source = "owner.name", target = "ownerName")
@Mapping(source = "owner.id", target = "ownerId")
WorkflowDetailedDTO toDetailedDto(Workflow workflow);

Workflow updateEntity(WorkflowDTO workflowDTO, @MappingTarget Workflow workflow);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.flexwork.modules.teams.domain.WorkflowState;
import io.flexwork.modules.teams.service.dto.WorkflowStateDTO;
import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

Expand All @@ -13,4 +14,6 @@ public interface WorkflowStateMapper {

@Mapping(source = "workflowId", target = "workflow.id")
WorkflowState toEntity(WorkflowStateDTO workflowStateDTO);

List<WorkflowStateDTO> toDTOList(List<WorkflowState> states);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.flexwork.modules.teams.service.mapper;

import io.flexwork.modules.teams.domain.WorkflowTransition;
import io.flexwork.modules.teams.service.dto.WorkflowTransitionDTO;
import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(componentModel = "spring")
public interface WorkflowTransitionMapper {

@Mapping(source = "sourceState.id", target = "sourceStateId")
@Mapping(source = "targetState.id", target = "targetStateId")
@Mapping(source = "workflow.id", target = "workflowId")
WorkflowTransitionDTO toDTO(WorkflowTransition transition);

WorkflowTransition toEntity(WorkflowTransitionDTO transitionDTO);

List<WorkflowTransitionDTO> toDTOList(List<WorkflowTransition> transitions);
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,8 @@ public ResponseEntity<TeamDTO> updateTeam(
}
TeamDTO updatedTeam = teamService.updateTeam(team);
// Remove the old logo
if (fileRemovedPath.isPresent()) {
eventPublisher.publishEvent(new ResourceRemoveEvent(this, fileRemovedPath.get()));
}
fileRemovedPath.ifPresent(
s -> eventPublisher.publishEvent(new ResourceRemoveEvent(this, s)));
return ResponseEntity.ok(updatedTeam);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public Long countOverdueTickets(@PathVariable Long teamId) {
public List<TicketActionCountByDateDTO> getTicketCreationDaySeries(
@PathVariable Long teamId,
@RequestParam(required = false, defaultValue = "7") int days) {
return teamRequestService.getTicketCreationTimeseries(teamId, days);
return teamRequestService.getTicketCreationTimeSeries(teamId, days);
}

@GetMapping("/users/{userId}/overdue-tickets")
Expand Down
Loading

0 comments on commit 2146de4

Please sign in to comment.