Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class ReactShellFilter implements Filter {
Pattern.compile("^/attachments/[^/]+$"), Pattern.compile("^/companies(?:$|/new$|/[^/]+(?:/edit)?$)"),
Pattern.compile("^/articles(?:$|/new$|/[^/]+(?:/edit)?$)"),
Pattern.compile("^/categories(?:$|/new$|/[^/]+(?:/edit)?$)"),
Pattern.compile("^/custom-fields(?:$|/new$|/[^/]+/edit$)"),
Pattern.compile("^/entitlements(?:$|/new$|/[^/]+(?:/edit)?$)"),
Pattern.compile("^/levels(?:$|/new$|/[^/]+(?:/edit)?$)"),
Pattern.compile("^/support/tickets(?:$|/open$|/closed$|/new$|/[^/]+$)"),
Expand Down Expand Up @@ -92,8 +93,8 @@ private boolean acceptsReactShell(String accept, String path) {
private boolean isLegacyBaseRoute(String path) {
return switch (path) {
case "/", "/login", "/error", "/not-found", "/profile", "/reports", "/owner", "/users", "/companies",
"/articles", "/categories", "/entitlements", "/levels", "/support/tickets", "/support/users",
"/tam/users", "/user/tickets", "/superuser/tickets", "/superuser/users" -> true;
"/articles", "/categories", "/custom-fields", "/entitlements", "/levels", "/support/tickets",
"/support/users", "/tam/users", "/user/tickets", "/superuser/tickets", "/superuser/users" -> true;
default -> false;
};
}
Expand Down
64 changes: 64 additions & 0 deletions src/backend/main/java/ai/mnemosyne_systems/model/CustomField.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Eclipse Public License - v 2.0
*
* THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
* PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
* OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
*/

package ai.mnemosyne_systems.model;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "custom_fields", uniqueConstraints = @UniqueConstraint(name = "uk_custom_fields_name_key", columnNames = "name_key"))
public class CustomField extends PanacheEntityBase {

public static final String TYPE_TEXT = "TEXT";
public static final String TYPE_NUMBER = "NUMBER";
public static final String TYPE_DATE = "DATE";
public static final String TYPE_SELECT = "SELECT";

@Id
@SequenceGenerator(name = "custom_field_seq", sequenceName = "custom_field_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "custom_field_seq")
public Long id;

@Column(nullable = false)
public String name;

@Column(name = "name_key", nullable = false)
public String nameKey;

@Column(name = "field_type", nullable = false, length = 16)
public String fieldType;

@Column(name = "is_required", nullable = false, columnDefinition = "boolean default false")
public boolean required;

@Column(columnDefinition = "text")
public String description;

@Column(nullable = false)
public boolean active = true;

@Column(name = "sort_order", nullable = false)
public int sortOrder;

@OneToMany(mappedBy = "customField", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("sortOrder asc, id asc")
public List<CustomFieldOption> options = new ArrayList<>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Eclipse Public License - v 2.0
*
* THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
* PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
* OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
*/

package ai.mnemosyne_systems.model;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;

@Entity
@Table(name = "custom_field_options", uniqueConstraints = @UniqueConstraint(name = "uk_custom_field_option_label", columnNames = {
"custom_field_id", "label" }))
public class CustomFieldOption extends PanacheEntityBase {

@Id
@SequenceGenerator(name = "custom_field_option_seq", sequenceName = "custom_field_option_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "custom_field_option_seq")
public Long id;

@ManyToOne(optional = false)
@JoinColumn(name = "custom_field_id", nullable = false)
public CustomField customField;

@Column(nullable = false, columnDefinition = "text")
public String label;

@Column(name = "sort_order", nullable = false)
public int sortOrder;
}
3 changes: 3 additions & 0 deletions src/backend/main/java/ai/mnemosyne_systems/model/Ticket.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ public class Ticket extends PanacheEntityBase {
@OneToMany(mappedBy = "ticket", cascade = CascadeType.ALL, orphanRemoval = true)
public List<Message> messages = new ArrayList<>();

@OneToMany(mappedBy = "ticket", cascade = CascadeType.ALL, orphanRemoval = true)
public List<TicketCustomFieldValue> customFieldValues = new ArrayList<>();

@ManyToMany
@JoinTable(name = "ticket_supports", joinColumns = @JoinColumn(name = "ticket_id"), inverseJoinColumns = @JoinColumn(name = "user_id"))
public List<User> supportUsers = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Eclipse Public License - v 2.0
*
* THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
* PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
* OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
*/

package ai.mnemosyne_systems.model;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal;
import java.time.LocalDate;

@Entity
@Table(name = "ticket_custom_field_values", uniqueConstraints = @UniqueConstraint(name = "uk_ticket_custom_field", columnNames = {
"ticket_id", "custom_field_id" }), indexes = {
@Index(name = "idx_ticket_custom_field_text", columnList = "custom_field_id, text_value"),
@Index(name = "idx_ticket_custom_field_number", columnList = "custom_field_id, number_value"),
@Index(name = "idx_ticket_custom_field_date", columnList = "custom_field_id, date_value") })
public class TicketCustomFieldValue extends PanacheEntityBase {

@Id
@SequenceGenerator(name = "ticket_custom_field_value_seq", sequenceName = "ticket_custom_field_value_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ticket_custom_field_value_seq")
public Long id;

@ManyToOne(optional = false)
@JoinColumn(name = "ticket_id", nullable = false)
public Ticket ticket;

@ManyToOne(optional = false)
@JoinColumn(name = "custom_field_id", nullable = false)
public CustomField customField;

@Column(name = "text_value", columnDefinition = "text")
public String textValue;

@Column(name = "number_value", precision = 38, scale = 10)
public BigDecimal numberValue;

@Column(name = "date_value")
public LocalDate dateValue;
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,8 @@ private EventConstants() {
public static final long VERSION_DELETED = 133L;
public static final long USER_ACTIVATED = 134L;
public static final long USER_DEACTIVATED = 135L;
public static final long CUSTOM_FIELD_CREATED = 136L;
public static final long CUSTOM_FIELD_UPDATED = 137L;
public static final long CUSTOM_FIELD_DEACTIVATED = 138L;
public static final long CUSTOM_FIELD_DELETED = 139L;
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ private List<NavLink> navigation(User user) {
return List.of(new NavLink("Dashboard", "/"), new NavLink("Owner", "/owner"),
new NavLink("Companies", "/companies"), new NavLink("Tickets", "/tickets"),
new NavLink("Articles", "/articles"), new NavLink("Categories", "/categories"),
new NavLink("Entitlements", "/entitlements"), new NavLink("Levels", "/levels"),
new NavLink("Users", "/users"), new NavLink("Profile", "/profile"),
new NavLink("Reports", "/reports"));
new NavLink("Custom fields", "/custom-fields"), new NavLink("Entitlements", "/entitlements"),
new NavLink("Levels", "/levels"), new NavLink("Users", "/users"),
new NavLink("Profile", "/profile"), new NavLink("Reports", "/reports"));
}
if (AuthHelper.isSupport(user)) {
return List.of(new NavLink("Tickets", "/support/tickets"), new NavLink("Articles", "/articles"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Eclipse Public License - v 2.0
*
* THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
* PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
* OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
*/

package ai.mnemosyne_systems.resource;

import ai.mnemosyne_systems.model.CustomField;
import ai.mnemosyne_systems.service.TicketCustomFieldService;
import io.smallrye.common.annotation.Blocking;
import jakarta.annotation.security.RolesAllowed;
import jakarta.transaction.Transactional;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;

@Path("/api/custom-fields")
@Produces(MediaType.APPLICATION_JSON)
@Blocking
@RolesAllowed("admin")
public class CustomFieldApiResource {

@GET
@Transactional
public CustomFieldListResponse list() {
List<CustomFieldSummary> items = CustomField.<CustomField> find(
"select distinct cf from CustomField cf left join fetch cf.options order by cf.sortOrder asc, cf.id asc")
.list().stream().map(this::toSummary).toList();
return new CustomFieldListResponse(true, "/custom-fields/new", items);
}

@GET
@Path("/bootstrap")
public CustomFieldBootstrapResponse bootstrap() {
return new CustomFieldBootstrapResponse(true, TicketCustomFieldService.FIELD_TYPES);
}

@GET
@Path("/{id}")
@Transactional
public CustomFieldDetailResponse detail(@PathParam("id") Long id) {
CustomField field = CustomField
.find("select distinct cf from CustomField cf left join fetch cf.options where cf.id = ?1", id)
.firstResult();
if (field == null) {
throw new NotFoundException();
}
return new CustomFieldDetailResponse(field.id, field.name, field.fieldType, field.required, field.description,
field.active, field.sortOrder, options(field), "/custom-fields/" + field.id + "/edit",
"/custom-fields/" + field.id + "/deactivate");
}

private CustomFieldSummary toSummary(CustomField field) {
return new CustomFieldSummary(field.id, field.name, field.fieldType, field.required, field.active,
field.sortOrder, field.description, options(field), "/custom-fields/" + field.id + "/edit",
"/custom-fields/" + field.id + "/deactivate");
}

private List<String> options(CustomField field) {
return field.options == null ? List.of() : field.options.stream().map(option -> option.label).toList();
}

public record CustomFieldListResponse(boolean canCreate, String createPath, List<CustomFieldSummary> items) {
}

public record CustomFieldBootstrapResponse(boolean canEdit, List<String> fieldTypes) {
}

public record CustomFieldSummary(Long id, String name, String fieldType, boolean required, boolean active,
int sortOrder, String description, List<String> options, String editPath, String deactivatePath) {
}

public record CustomFieldDetailResponse(Long id, String name, String fieldType, boolean required,
String description, boolean active, int sortOrder, List<String> options, String editPath,
String deactivatePath) {
}
}
Loading
Loading