forked from casdoor/casdoor
feat: add ticket list/edit pages (#4651)
This commit is contained in:
271
controllers/ticket.go
Normal file
271
controllers/ticket.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetTickets
|
||||
// @Title GetTickets
|
||||
// @Tag Ticket API
|
||||
// @Description get tickets
|
||||
// @Param owner query string true "The owner of tickets"
|
||||
// @Success 200 {array} object.Ticket The Response object
|
||||
// @router /get-tickets [get]
|
||||
func (c *ApiController) GetTickets() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
var tickets []*object.Ticket
|
||||
var err error
|
||||
|
||||
if limit == "" || page == "" {
|
||||
if isAdmin {
|
||||
tickets, err = object.GetTickets(owner)
|
||||
} else {
|
||||
tickets, err = object.GetUserTickets(owner, user.GetId())
|
||||
}
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(tickets)
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
var count int64
|
||||
|
||||
if isAdmin {
|
||||
count, err = object.GetTicketCount(owner, field, value)
|
||||
} else {
|
||||
// For non-admin users, only show their own tickets
|
||||
tickets, err = object.GetUserTickets(owner, user.GetId())
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
count = int64(len(tickets))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
|
||||
if isAdmin {
|
||||
tickets, err = object.GetPaginationTickets(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(tickets, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetTicket
|
||||
// @Title GetTicket
|
||||
// @Tag Ticket API
|
||||
// @Description get ticket
|
||||
// @Param id query string true "The id ( owner/name ) of the ticket"
|
||||
// @Success 200 {object} object.Ticket The Response object
|
||||
// @router /get-ticket [get]
|
||||
func (c *ApiController) GetTicket() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
ticket, err := object.GetTicket(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission: user can only view their own tickets unless they are admin
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
if ticket != nil && !isAdmin && ticket.User != user.GetId() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(ticket)
|
||||
}
|
||||
|
||||
// UpdateTicket
|
||||
// @Title UpdateTicket
|
||||
// @Tag Ticket API
|
||||
// @Description update ticket
|
||||
// @Param id query string true "The id ( owner/name ) of the ticket"
|
||||
// @Param body body object.Ticket true "The details of the ticket"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-ticket [post]
|
||||
func (c *ApiController) UpdateTicket() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
var ticket object.Ticket
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ticket)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
existingTicket, err := object.GetTicket(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if existingTicket == nil {
|
||||
c.ResponseError(c.T("ticket:Ticket not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Normal users can only close their own tickets
|
||||
if !isAdmin {
|
||||
if existingTicket.User != user.GetId() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
// Normal users can only change state to "Closed"
|
||||
if ticket.State != "Closed" && ticket.State != existingTicket.State {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
// Preserve original fields that users shouldn't modify
|
||||
ticket.Owner = existingTicket.Owner
|
||||
ticket.Name = existingTicket.Name
|
||||
ticket.User = existingTicket.User
|
||||
ticket.CreatedTime = existingTicket.CreatedTime
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateTicket(id, &ticket))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddTicket
|
||||
// @Title AddTicket
|
||||
// @Tag Ticket API
|
||||
// @Description add ticket
|
||||
// @Param body body object.Ticket true "The details of the ticket"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-ticket [post]
|
||||
func (c *ApiController) AddTicket() {
|
||||
var ticket object.Ticket
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ticket)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set the user field to the current user
|
||||
user := c.getCurrentUser()
|
||||
ticket.User = user.GetId()
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddTicket(&ticket))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteTicket
|
||||
// @Title DeleteTicket
|
||||
// @Tag Ticket API
|
||||
// @Description delete ticket
|
||||
// @Param body body object.Ticket true "The details of the ticket"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-ticket [post]
|
||||
func (c *ApiController) DeleteTicket() {
|
||||
var ticket object.Ticket
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &ticket)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Only admins can delete tickets
|
||||
if !c.IsAdmin() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteTicket(&ticket))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddTicketMessage
|
||||
// @Title AddTicketMessage
|
||||
// @Tag Ticket API
|
||||
// @Description add a message to a ticket
|
||||
// @Param id query string true "The id ( owner/name ) of the ticket"
|
||||
// @Param body body object.TicketMessage true "The message to add"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-ticket-message [post]
|
||||
func (c *ApiController) AddTicketMessage() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
var message object.TicketMessage
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &message)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission
|
||||
user := c.getCurrentUser()
|
||||
isAdmin := c.IsAdmin()
|
||||
|
||||
ticket, err := object.GetTicket(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if ticket == nil {
|
||||
c.ResponseError(c.T("ticket:Ticket not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Users can only add messages to their own tickets, admins can add to any ticket
|
||||
if !isAdmin && ticket.User != user.GetId() {
|
||||
c.ResponseError(c.T("auth:Unauthorized operation"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set the author and admin flag
|
||||
message.Author = user.GetId()
|
||||
message.IsAdmin = isAdmin
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddTicketMessage(id, &message))
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -448,4 +448,9 @@ func (a *Ormer) createTable() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Ticket))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
162
object/ticket.go
Normal file
162
object/ticket.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
type TicketMessage struct {
|
||||
Author string `json:"author"`
|
||||
Text string `json:"text"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
}
|
||||
|
||||
type Ticket struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
User string `xorm:"varchar(100) index" json:"user"`
|
||||
Title string `xorm:"varchar(200)" json:"title"`
|
||||
Content string `xorm:"mediumtext" json:"content"`
|
||||
State string `xorm:"varchar(50)" json:"state"`
|
||||
Messages []*TicketMessage `xorm:"mediumtext json" json:"messages"`
|
||||
}
|
||||
|
||||
func GetTicketCount(owner, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
return session.Count(&Ticket{})
|
||||
}
|
||||
|
||||
func GetTickets(owner string) ([]*Ticket, error) {
|
||||
tickets := []*Ticket{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&tickets, &Ticket{Owner: owner})
|
||||
if err != nil {
|
||||
return tickets, err
|
||||
}
|
||||
|
||||
return tickets, nil
|
||||
}
|
||||
|
||||
func GetPaginationTickets(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Ticket, error) {
|
||||
tickets := []*Ticket{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&tickets)
|
||||
if err != nil {
|
||||
return tickets, err
|
||||
}
|
||||
|
||||
return tickets, nil
|
||||
}
|
||||
|
||||
func GetUserTickets(owner, user string) ([]*Ticket, error) {
|
||||
tickets := []*Ticket{}
|
||||
err := ormer.Engine.Desc("created_time").Find(&tickets, &Ticket{Owner: owner, User: user})
|
||||
if err != nil {
|
||||
return tickets, err
|
||||
}
|
||||
|
||||
return tickets, nil
|
||||
}
|
||||
|
||||
func getTicket(owner string, name string) (*Ticket, error) {
|
||||
if owner == "" || name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ticket := Ticket{Owner: owner, Name: name}
|
||||
existed, err := ormer.Engine.Get(&ticket)
|
||||
if err != nil {
|
||||
return &ticket, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &ticket, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func GetTicket(id string) (*Ticket, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getTicket(owner, name)
|
||||
}
|
||||
|
||||
func UpdateTicket(id string, ticket *Ticket) (bool, error) {
|
||||
owner, name, err := util.GetOwnerAndNameFromIdWithError(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if t, err := getTicket(owner, name); err != nil {
|
||||
return false, err
|
||||
} else if t == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(ticket)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func AddTicket(ticket *Ticket) (bool, error) {
|
||||
affected, err := ormer.Engine.Insert(ticket)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func DeleteTicket(ticket *Ticket) (bool, error) {
|
||||
affected, err := ormer.Engine.ID(core.PK{ticket.Owner, ticket.Name}).Delete(&Ticket{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (ticket *Ticket) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", ticket.Owner, ticket.Name)
|
||||
}
|
||||
|
||||
func AddTicketMessage(id string, message *TicketMessage) (bool, error) {
|
||||
ticket, err := GetTicket(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if ticket == nil {
|
||||
return false, fmt.Errorf("ticket not found: %s", id)
|
||||
}
|
||||
|
||||
if ticket.Messages == nil {
|
||||
ticket.Messages = []*TicketMessage{}
|
||||
}
|
||||
|
||||
ticket.Messages = append(ticket.Messages, message)
|
||||
return UpdateTicket(id, ticket)
|
||||
}
|
||||
@@ -271,6 +271,13 @@ func initAPI() {
|
||||
beego.Router("/api/add-webhook", &controllers.ApiController{}, "POST:AddWebhook")
|
||||
beego.Router("/api/delete-webhook", &controllers.ApiController{}, "POST:DeleteWebhook")
|
||||
|
||||
beego.Router("/api/get-tickets", &controllers.ApiController{}, "GET:GetTickets")
|
||||
beego.Router("/api/get-ticket", &controllers.ApiController{}, "GET:GetTicket")
|
||||
beego.Router("/api/update-ticket", &controllers.ApiController{}, "POST:UpdateTicket")
|
||||
beego.Router("/api/add-ticket", &controllers.ApiController{}, "POST:AddTicket")
|
||||
beego.Router("/api/delete-ticket", &controllers.ApiController{}, "POST:DeleteTicket")
|
||||
beego.Router("/api/add-ticket-message", &controllers.ApiController{}, "POST:AddTicketMessage")
|
||||
|
||||
beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword")
|
||||
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
|
||||
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "GET:GetEmailAndPhone")
|
||||
|
||||
@@ -99,6 +99,8 @@ import {clearWeb3AuthToken} from "./auth/Web3Auth";
|
||||
import TransactionListPage from "./TransactionListPage";
|
||||
import TransactionEditPage from "./TransactionEditPage";
|
||||
import VerificationListPage from "./VerificationListPage";
|
||||
import TicketListPage from "./TicketListPage";
|
||||
import TicketEditPage from "./TicketEditPage";
|
||||
|
||||
function ManagementPage(props) {
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
@@ -343,12 +345,14 @@ function ManagementPage(props) {
|
||||
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
|
||||
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"),
|
||||
Setting.getItem(<Link to="/tickets">{i18next.t("general:Tickets")}</Link>, "/tickets"),
|
||||
Setting.getItem(<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>{i18next.t("general:Swagger")}</a>, "/swagger")]));
|
||||
} else {
|
||||
res.push(Setting.getItem(<Link style={{color: textColor}} to="/syncers">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone twoToneColor={twoToneColor} />, [
|
||||
Setting.getItem(<Link to="/forms">{i18next.t("general:Forms")}</Link>, "/forms"),
|
||||
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks")]));
|
||||
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"),
|
||||
Setting.getItem(<Link to="/tickets">{i18next.t("general:Tickets")}</Link>, "/tickets")]));
|
||||
}
|
||||
|
||||
if (navItemsIsAll()) {
|
||||
@@ -483,6 +487,8 @@ function ManagementPage(props) {
|
||||
<Route exact path="/transactions/:organizationName/:transactionName" render={(props) => renderLoginIfNotLoggedIn(<TransactionEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/webhooks" render={(props) => renderLoginIfNotLoggedIn(<WebhookListPage account={account} {...props} />)} />
|
||||
<Route exact path="/webhooks/:webhookName" render={(props) => renderLoginIfNotLoggedIn(<WebhookEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/tickets" render={(props) => renderLoginIfNotLoggedIn(<TicketListPage account={account} {...props} />)} />
|
||||
<Route exact path="/tickets/:organizationName/:ticketName" render={(props) => renderLoginIfNotLoggedIn(<TicketEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/ldap/:organizationName/:ldapId" render={(props) => renderLoginIfNotLoggedIn(<LdapEditPage account={account} {...props} />)} />
|
||||
<Route exact path="/ldap/sync/:organizationName/:ldapId" render={(props) => renderLoginIfNotLoggedIn(<LdapSyncPage account={account} {...props} />)} />
|
||||
<Route exact path="/mfa/setup" render={(props) => renderLoginIfNotLoggedIn(<MfaSetupPage account={account} onfinish={onfinish} {...props} />)} />
|
||||
|
||||
318
web/src/TicketEditPage.js
Normal file
318
web/src/TicketEditPage.js
Normal file
@@ -0,0 +1,318 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Avatar, Button, Card, Col, Divider, Input, List, Row, Select, Space, Tag} from "antd";
|
||||
import {SendOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import * as TicketBackend from "./backend/TicketBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import moment from "moment";
|
||||
|
||||
const {Option} = Select;
|
||||
const {TextArea} = Input;
|
||||
|
||||
class TicketEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
ticketName: props.match.params.ticketName,
|
||||
ticket: null,
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
messageText: "",
|
||||
sending: false,
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getTicket();
|
||||
}
|
||||
|
||||
getTicket() {
|
||||
TicketBackend.getTicket(this.state.organizationName, this.state.ticketName)
|
||||
.then((res) => {
|
||||
if (res.data === null) {
|
||||
this.props.history.push("/404");
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.data.messages === null) {
|
||||
res.data.messages = [];
|
||||
}
|
||||
|
||||
this.setState({
|
||||
ticket: res.data,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parseTicketField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updateTicketField(key, value) {
|
||||
value = this.parseTicketField(key, value);
|
||||
|
||||
const ticket = this.state.ticket;
|
||||
ticket[key] = value;
|
||||
this.setState({
|
||||
ticket: ticket,
|
||||
});
|
||||
}
|
||||
|
||||
submitTicketEdit(willExist) {
|
||||
const ticket = Setting.deepCopy(this.state.ticket);
|
||||
TicketBackend.updateTicket(this.state.organizationName, this.state.ticketName, ticket)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
this.setState({
|
||||
ticketName: this.state.ticket.name,
|
||||
});
|
||||
if (willExist) {
|
||||
this.props.history.push("/tickets");
|
||||
} else {
|
||||
this.props.history.push(`/tickets/${this.state.ticket.owner}/${this.state.ticket.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
this.updateTicketField("name", this.state.ticketName);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
sendMessage() {
|
||||
if (!this.state.messageText.trim()) {
|
||||
Setting.showMessage("error", i18next.t("ticket:Please enter a message"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({sending: true});
|
||||
|
||||
const message = {
|
||||
author: this.props.account.name,
|
||||
text: this.state.messageText,
|
||||
timestamp: moment().format(),
|
||||
isAdmin: Setting.isAdminUser(this.props.account),
|
||||
};
|
||||
|
||||
TicketBackend.addTicketMessage(this.state.organizationName, this.state.ticketName, message)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully sent"));
|
||||
this.setState({
|
||||
messageText: "",
|
||||
sending: false,
|
||||
});
|
||||
this.getTicket();
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to send")}: ${res.msg}`);
|
||||
this.setState({sending: false});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
this.setState({sending: false});
|
||||
});
|
||||
}
|
||||
|
||||
renderTicket() {
|
||||
const isAdmin = Setting.isAdminUser(this.props.account);
|
||||
const isOwner = this.props.account.name === this.state.ticket?.user;
|
||||
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("ticket:New Ticket") : i18next.t("ticket:Edit Ticket")}
|
||||
<Button onClick={() => this.submitTicketEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitTicketEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
</div>
|
||||
} style={{marginLeft: "5px"}} type="inner">
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Organization")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.ticket.owner} disabled={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.ticket.name} disabled={!isAdmin} onChange={e => {
|
||||
this.updateTicketField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Display name")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.ticket.displayName} onChange={e => {
|
||||
this.updateTicketField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Title")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.ticket.title} disabled={!isAdmin && !isOwner} onChange={e => {
|
||||
this.updateTicketField("title", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Content")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<TextArea autoSize={{minRows: 3, maxRows: 10}} value={this.state.ticket.content} disabled={!isAdmin && !isOwner} onChange={e => {
|
||||
this.updateTicketField("content", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:User")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.ticket.user} disabled={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:State")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.state.ticket.state}
|
||||
disabled={!isAdmin && this.state.ticket.state === "Closed"}
|
||||
onChange={(value => {
|
||||
this.updateTicketField("state", value);
|
||||
})}>
|
||||
<Option value="Open">{i18next.t("ticket:Open")}</Option>
|
||||
<Option value="In Progress">{i18next.t("ticket:In Progress")}</Option>
|
||||
<Option value="Resolved">{i18next.t("ticket:Resolved")}</Option>
|
||||
<Option value="Closed">{i18next.t("ticket:Closed")}</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Created time")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.ticket.createdTime} disabled={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Updated time")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.ticket.updatedTime} disabled={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
renderMessages() {
|
||||
return (
|
||||
<Card size="small" title={i18next.t("ticket:Messages")} style={{marginTop: "20px", marginLeft: "5px"}} type="inner">
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={this.state.ticket.messages || []}
|
||||
renderItem={(message, index) => (
|
||||
<List.Item key={index}>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar icon={<UserOutlined />} style={{backgroundColor: message.isAdmin ? "#1890ff" : "#87d068"}} />}
|
||||
title={
|
||||
<Space>
|
||||
<span>{message.author}</span>
|
||||
{message.isAdmin && <Tag color="blue">{i18next.t("general:Admin")}</Tag>}
|
||||
<span style={{fontSize: "12px", color: "#999"}}>{Setting.getFormattedDate(message.timestamp)}</span>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<div style={{whiteSpace: "pre-wrap", wordBreak: "break-word"}}>
|
||||
{message.text}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<Divider />
|
||||
<Row gutter={16}>
|
||||
<Col span={20}>
|
||||
<TextArea
|
||||
rows={3}
|
||||
value={this.state.messageText}
|
||||
onChange={e => this.setState({messageText: e.target.value})}
|
||||
placeholder={i18next.t("ticket:Type your message here...")}
|
||||
onPressEnter={(e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
this.sendMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
loading={this.state.sending}
|
||||
onClick={() => this.sendMessage()}
|
||||
style={{width: "100%", height: "100%"}}
|
||||
>
|
||||
{i18next.t("general:Send")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<div style={{marginTop: "8px", color: "#999", fontSize: "12px"}}>
|
||||
{i18next.t("ticket:Press Ctrl+Enter to send")}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.ticket !== null ? this.renderTicket() : null
|
||||
}
|
||||
<br />
|
||||
{
|
||||
this.state.ticket !== null ? this.renderMessages() : null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TicketEditPage;
|
||||
253
web/src/TicketListPage.js
Normal file
253
web/src/TicketListPage.js
Normal file
@@ -0,0 +1,253 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import {CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, SyncOutlined} from "@ant-design/icons";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as TicketBackend from "./backend/TicketBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
|
||||
class TicketListPage extends BaseListPage {
|
||||
newTicket() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = Setting.getRequestOrganization(this.props.account);
|
||||
return {
|
||||
owner: owner,
|
||||
name: `ticket_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
updatedTime: moment().format(),
|
||||
displayName: `New Ticket - ${randomName}`,
|
||||
user: this.props.account.name,
|
||||
title: "",
|
||||
content: "",
|
||||
state: "Open",
|
||||
messages: [],
|
||||
};
|
||||
}
|
||||
|
||||
addTicket() {
|
||||
const newTicket = this.newTicket();
|
||||
TicketBackend.addTicket(newTicket)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/tickets/${newTicket.owner}/${newTicket.name}`, mode: "add"});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteTicket(i) {
|
||||
TicketBackend.deleteTicket(this.state.data[i])
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.fetch({
|
||||
pagination: {
|
||||
...this.state.pagination,
|
||||
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(tickets) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "140px",
|
||||
fixed: "left",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/tickets/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
width: "200px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Title"),
|
||||
dataIndex: "title",
|
||||
key: "title",
|
||||
width: "200px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("title"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:User"),
|
||||
dataIndex: "user",
|
||||
key: "user",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("user"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/users/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Updated time"),
|
||||
dataIndex: "updatedTime",
|
||||
key: "updatedTime",
|
||||
width: "160px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:State"),
|
||||
dataIndex: "state",
|
||||
key: "state",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("state"),
|
||||
render: (text, record, index) => {
|
||||
switch (text) {
|
||||
case "Open":
|
||||
return Setting.getTag("processing", i18next.t("ticket:Open"), <ClockCircleOutlined />);
|
||||
case "In Progress":
|
||||
return Setting.getTag("warning", i18next.t("ticket:In Progress"), <SyncOutlined spin />);
|
||||
case "Resolved":
|
||||
return Setting.getTag("success", i18next.t("ticket:Resolved"), <CheckCircleOutlined />);
|
||||
case "Closed":
|
||||
return Setting.getTag("default", i18next.t("ticket:Closed"), <CloseCircleOutlined />);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "180px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/tickets/${record.owner}/${record.name}`)}>{i18next.t("general:View")}</Button>
|
||||
{Setting.isAdminUser(this.props.account) ? (
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteTicket(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const paginationProps = {
|
||||
total: this.state.pagination.total,
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={tickets} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Tickets")}
|
||||
<Button type="primary" size="small" onClick={this.addTicket.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
let field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (params.type !== undefined && params.type !== null) {
|
||||
field = "type";
|
||||
value = params.type;
|
||||
}
|
||||
this.setState({loading: true});
|
||||
TicketBackend.getTickets(Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (Setting.isResponseDenied(res)) {
|
||||
this.setState({
|
||||
isAuthorized: false,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default TicketListPage;
|
||||
82
web/src/backend/TicketBackend.js
Normal file
82
web/src/backend/TicketBackend.js
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function getTickets(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-tickets?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getTicket(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-ticket?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateTicket(owner, name, ticket) {
|
||||
const newTicket = Setting.deepCopy(ticket);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-ticket?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newTicket),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addTicket(ticket) {
|
||||
const newTicket = Setting.deepCopy(ticket);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-ticket`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newTicket),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteTicket(ticket) {
|
||||
const newTicket = Setting.deepCopy(ticket);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-ticket`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newTicket),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addTicketMessage(owner, name, message) {
|
||||
return fetch(`${Setting.ServerUrl}/api/add-ticket-message?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(message),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
@@ -488,6 +488,7 @@
|
||||
"Syncers": "Syncers",
|
||||
"System Info": "System Info",
|
||||
"Tab": "Tab",
|
||||
"Tickets": "Tickets",
|
||||
"The actions cannot be empty": "The actions cannot be empty",
|
||||
"The resources cannot be empty": "The resources cannot be empty",
|
||||
"The users and roles cannot be empty at the same time": "The users and roles cannot be empty at the same time",
|
||||
@@ -522,6 +523,7 @@
|
||||
"Users - Tooltip": "Users",
|
||||
"Users under all organizations": "Users under all organizations",
|
||||
"Verifications": "Verifications",
|
||||
"View": "View",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "You can only select one physical group",
|
||||
"You must select a picture first": "You must select a picture first",
|
||||
@@ -1392,6 +1394,19 @@
|
||||
"You have changed the username, please save your change first before modifying the password": "You have changed the username, please save your change first before modifying the password",
|
||||
"input password": "input password"
|
||||
},
|
||||
"ticket": {
|
||||
"Closed": "Closed",
|
||||
"Edit Ticket": "Edit Ticket",
|
||||
"In Progress": "In Progress",
|
||||
"Messages": "Messages",
|
||||
"New Ticket": "New Ticket",
|
||||
"Open": "Open",
|
||||
"Please enter a message": "Please enter a message",
|
||||
"Press Ctrl+Enter to send": "Press Ctrl+Enter to send",
|
||||
"Resolved": "Resolved",
|
||||
"Ticket not found": "Ticket not found",
|
||||
"Type your message here...": "Type your message here..."
|
||||
},
|
||||
"verification": {
|
||||
"Is used": "Is used",
|
||||
"Receiver": "Receiver"
|
||||
|
||||
Reference in New Issue
Block a user