added a common badge type when initializing the plugin, moved the ability to create types/badges to the UI, and redesigned components (AllBadgesRow, UserBadgeRow, UserRow, BadgeDetails)
This commit is contained in:
parent
04a001bc94
commit
e47a63f1d5
@ -3,6 +3,7 @@ package badgesmodel
|
|||||||
const (
|
const (
|
||||||
NameMaxLength = 20
|
NameMaxLength = 20
|
||||||
DescriptionMaxLength = 120
|
DescriptionMaxLength = 120
|
||||||
|
DefaultTypeName = "Общий"
|
||||||
|
|
||||||
ImageTypeEmoji ImageType = "emoji"
|
ImageTypeEmoji ImageType = "emoji"
|
||||||
ImageTypeRelativeURL ImageType = "rel_url"
|
ImageTypeRelativeURL ImageType = "rel_url"
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package badgesmodel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BadgeType string
|
type BadgeType string
|
||||||
@ -56,6 +57,7 @@ type BadgeTypeDefinition struct {
|
|||||||
CreatedBy string `json:"created_by"`
|
CreatedBy string `json:"created_by"`
|
||||||
CanGrant PermissionScheme `json:"can_grant"`
|
CanGrant PermissionScheme `json:"can_grant"`
|
||||||
CanCreate PermissionScheme `json:"can_create"`
|
CanCreate PermissionScheme `json:"can_create"`
|
||||||
|
IsDefault bool `json:"is_default"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PermissionScheme struct {
|
type PermissionScheme struct {
|
||||||
@ -87,8 +89,8 @@ type Subscription struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b Badge) IsValid() bool {
|
func (b Badge) IsValid() bool {
|
||||||
return len(b.Name) <= NameMaxLength &&
|
return utf8.RuneCountInString(b.Name) <= NameMaxLength &&
|
||||||
len(b.Description) <= DescriptionMaxLength &&
|
utf8.RuneCountInString(b.Description) <= DescriptionMaxLength &&
|
||||||
b.Image != ""
|
b.Image != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
400
server/api.go
400
server/api.go
@ -32,6 +32,41 @@ type APIErrorResponse struct {
|
|||||||
StatusCode int `json:"status_code"`
|
StatusCode int `json:"status_code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateBadgeRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Multiple bool `json:"multiple"`
|
||||||
|
ChannelID string `json:"channel_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateBadgeRequest struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Multiple bool `json:"multiple"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTypeRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
EveryoneCanCreate bool `json:"everyone_can_create"`
|
||||||
|
EveryoneCanGrant bool `json:"everyone_can_grant"`
|
||||||
|
ChannelID string `json:"channel_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeWithBadgeCount struct {
|
||||||
|
*badgesmodel.BadgeTypeDefinition
|
||||||
|
BadgeCount int `json:"badge_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetTypesResponse struct {
|
||||||
|
Types []TypeWithBadgeCount `json:"types"`
|
||||||
|
CanCreateType bool `json:"can_create_type"`
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Plugin) initializeAPI() {
|
func (p *Plugin) initializeAPI() {
|
||||||
p.router = mux.NewRouter()
|
p.router = mux.NewRouter()
|
||||||
p.router.Use(p.withRecovery)
|
p.router.Use(p.withRecovery)
|
||||||
@ -44,6 +79,12 @@ func (p *Plugin) initializeAPI() {
|
|||||||
apiRouter.HandleFunc("/getUserBadges/{userID}", p.extractUserMiddleWare(p.getUserBadges, ResponseTypeJSON)).Methods(http.MethodGet)
|
apiRouter.HandleFunc("/getUserBadges/{userID}", p.extractUserMiddleWare(p.getUserBadges, ResponseTypeJSON)).Methods(http.MethodGet)
|
||||||
apiRouter.HandleFunc("/getBadgeDetails/{badgeID}", p.extractUserMiddleWare(p.getBadgeDetails, ResponseTypeJSON)).Methods(http.MethodGet)
|
apiRouter.HandleFunc("/getBadgeDetails/{badgeID}", p.extractUserMiddleWare(p.getBadgeDetails, ResponseTypeJSON)).Methods(http.MethodGet)
|
||||||
apiRouter.HandleFunc("/getAllBadges", p.extractUserMiddleWare(p.getAllBadges, ResponseTypeJSON)).Methods(http.MethodGet)
|
apiRouter.HandleFunc("/getAllBadges", p.extractUserMiddleWare(p.getAllBadges, ResponseTypeJSON)).Methods(http.MethodGet)
|
||||||
|
apiRouter.HandleFunc("/getTypes", p.extractUserMiddleWare(p.getTypes, ResponseTypeJSON)).Methods(http.MethodGet)
|
||||||
|
apiRouter.HandleFunc("/createBadge", p.extractUserMiddleWare(p.apiCreateBadge, ResponseTypeJSON)).Methods(http.MethodPost)
|
||||||
|
apiRouter.HandleFunc("/createType", p.extractUserMiddleWare(p.apiCreateType, ResponseTypeJSON)).Methods(http.MethodPost)
|
||||||
|
apiRouter.HandleFunc("/updateBadge", p.extractUserMiddleWare(p.apiUpdateBadge, ResponseTypeJSON)).Methods(http.MethodPut)
|
||||||
|
apiRouter.HandleFunc("/deleteBadge/{badgeID}", p.extractUserMiddleWare(p.apiDeleteBadge, ResponseTypeJSON)).Methods(http.MethodDelete)
|
||||||
|
apiRouter.HandleFunc("/deleteType/{typeID}", p.extractUserMiddleWare(p.apiDeleteType, ResponseTypeJSON)).Methods(http.MethodDelete)
|
||||||
|
|
||||||
pluginAPIRouter.HandleFunc(badgesmodel.PluginAPIPathEnsure, checkPluginRequest(p.ensureBadges)).Methods(http.MethodPost)
|
pluginAPIRouter.HandleFunc(badgesmodel.PluginAPIPathEnsure, checkPluginRequest(p.ensureBadges)).Methods(http.MethodPost)
|
||||||
pluginAPIRouter.HandleFunc(badgesmodel.PluginAPIPathGrant, checkPluginRequest(p.grantBadge)).Methods(http.MethodPost)
|
pluginAPIRouter.HandleFunc(badgesmodel.PluginAPIPathGrant, checkPluginRequest(p.grantBadge)).Methods(http.MethodPost)
|
||||||
@ -91,6 +132,361 @@ func dialogKeepOpen(w http.ResponseWriter) {
|
|||||||
_, _ = w.Write(resp.ToJson())
|
_, _ = w.Write(resp.ToJson())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) getTypes(w http.ResponseWriter, r *http.Request, userID string) {
|
||||||
|
u, err := p.mm.User.Get(userID)
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
types, err := p.filterCreateBadgeTypes(u)
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_get_types", Message: err.Error(), StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
badges, err := p.store.GetRawBadges()
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_get_badges", Message: err.Error(), StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
badgeCountByType := map[badgesmodel.BadgeType]int{}
|
||||||
|
for _, badge := range badges {
|
||||||
|
badgeCountByType[badge.Type]++
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]TypeWithBadgeCount, len(types))
|
||||||
|
for i, t := range types {
|
||||||
|
result[i] = TypeWithBadgeCount{
|
||||||
|
BadgeTypeDefinition: t,
|
||||||
|
BadgeCount: badgeCountByType[t.ID],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := GetTypesResponse{
|
||||||
|
Types: result,
|
||||||
|
CanCreateType: canCreateType(u, p.badgeAdminUserID, false),
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := json.Marshal(resp)
|
||||||
|
_, _ = w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) apiCreateBadge(w http.ResponseWriter, r *http.Request, userID string) {
|
||||||
|
var req CreateBadgeRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "invalid_request", Message: "Invalid request body", StatusCode: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := p.mm.User.Get(userID)
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Name = strings.TrimSpace(req.Name)
|
||||||
|
req.Description = strings.TrimSpace(req.Description)
|
||||||
|
req.Image = strings.TrimSpace(req.Image)
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "invalid_name", Message: "Name is required", StatusCode: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if length := len(req.Image); length > 1 && req.Image[0] == ':' && req.Image[length-1] == ':' {
|
||||||
|
req.Image = req.Image[1 : length-1]
|
||||||
|
}
|
||||||
|
if req.Image == "" {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "invalid_image", Message: "Emoji is required", StatusCode: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := p.store.GetType(badgesmodel.BadgeType(req.Type))
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "type_not_found", Message: "Badge type not found", StatusCode: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canCreateBadge(user, p.badgeAdminUserID, t) {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "no_permission", Message: "No permission to create badge of this type", StatusCode: http.StatusForbidden,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toCreate := &badgesmodel.Badge{
|
||||||
|
Name: req.Name,
|
||||||
|
Description: req.Description,
|
||||||
|
Image: req.Image,
|
||||||
|
ImageType: badgesmodel.ImageTypeEmoji,
|
||||||
|
Multiple: req.Multiple,
|
||||||
|
Type: badgesmodel.BadgeType(req.Type),
|
||||||
|
CreatedBy: userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
created, err := p.store.AddBadge(toCreate)
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_create_badge", Message: err.Error(), StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ChannelID != "" {
|
||||||
|
T := p.getT(user.Locale)
|
||||||
|
p.mm.Post.SendEphemeralPost(userID, &model.Post{
|
||||||
|
UserId: p.BotUserID,
|
||||||
|
ChannelId: req.ChannelID,
|
||||||
|
Message: T("badges.api.badge_created", "Значок `%s` создан.", toCreate.Name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := json.Marshal(created)
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_, _ = w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) apiCreateType(w http.ResponseWriter, r *http.Request, userID string) {
|
||||||
|
var req CreateTypeRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "invalid_request", Message: "Invalid request body", StatusCode: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := p.mm.User.Get(userID)
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canCreateType(user, p.badgeAdminUserID, false) {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "no_permission", Message: "No permission to create type", StatusCode: http.StatusForbidden,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Name = strings.TrimSpace(req.Name)
|
||||||
|
if req.Name == "" {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "invalid_name", Message: "Type name is required", StatusCode: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toCreate := &badgesmodel.BadgeTypeDefinition{
|
||||||
|
Name: req.Name,
|
||||||
|
CreatedBy: userID,
|
||||||
|
}
|
||||||
|
toCreate.CanCreate.Everyone = req.EveryoneCanCreate
|
||||||
|
toCreate.CanGrant.Everyone = req.EveryoneCanGrant
|
||||||
|
|
||||||
|
created, err := p.store.AddType(toCreate)
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_create_type", Message: err.Error(), StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ChannelID != "" {
|
||||||
|
T := p.getT(user.Locale)
|
||||||
|
p.mm.Post.SendEphemeralPost(userID, &model.Post{
|
||||||
|
UserId: p.BotUserID,
|
||||||
|
ChannelId: req.ChannelID,
|
||||||
|
Message: T("badges.api.type_created", "Тип `%s` создан.", toCreate.Name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := json.Marshal(created)
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_, _ = w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) apiUpdateBadge(w http.ResponseWriter, r *http.Request, userID string) {
|
||||||
|
var req UpdateBadgeRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "invalid_request", Message: "Invalid request body", StatusCode: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := p.mm.User.Get(userID)
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
badge, err := p.store.GetBadge(badgesmodel.BadgeID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "badge_not_found", Message: "Badge not found", StatusCode: http.StatusNotFound,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canEditBadge(user, p.badgeAdminUserID, badge) {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "no_permission", Message: "No permission to edit this badge", StatusCode: http.StatusForbidden,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Name = strings.TrimSpace(req.Name)
|
||||||
|
req.Description = strings.TrimSpace(req.Description)
|
||||||
|
req.Image = strings.TrimSpace(req.Image)
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "invalid_name", Message: "Name is required", StatusCode: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if length := len(req.Image); length > 1 && req.Image[0] == ':' && req.Image[length-1] == ':' {
|
||||||
|
req.Image = req.Image[1 : length-1]
|
||||||
|
}
|
||||||
|
if req.Image == "" {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "invalid_image", Message: "Emoji is required", StatusCode: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
badge.Name = req.Name
|
||||||
|
badge.Description = req.Description
|
||||||
|
badge.Image = req.Image
|
||||||
|
badge.Type = badgesmodel.BadgeType(req.Type)
|
||||||
|
badge.Multiple = req.Multiple
|
||||||
|
|
||||||
|
if err := p.store.UpdateBadge(badge); err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_update_badge", Message: err.Error(), StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := json.Marshal(badge)
|
||||||
|
_, _ = w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) apiDeleteBadge(w http.ResponseWriter, r *http.Request, userID string) {
|
||||||
|
badgeID, ok := mux.Vars(r)["badgeID"]
|
||||||
|
if !ok {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "missing_badge_id", Message: "Missing badge ID", StatusCode: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := p.mm.User.Get(userID)
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
badge, err := p.store.GetBadge(badgesmodel.BadgeID(badgeID))
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "badge_not_found", Message: "Badge not found", StatusCode: http.StatusNotFound,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canEditBadge(user, p.badgeAdminUserID, badge) {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "no_permission", Message: "No permission to delete this badge", StatusCode: http.StatusForbidden,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.store.DeleteBadge(badgesmodel.BadgeID(badgeID)); err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_delete_badge", Message: err.Error(), StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = w.Write([]byte(`{"success": true}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) apiDeleteType(w http.ResponseWriter, r *http.Request, userID string) {
|
||||||
|
typeID, ok := mux.Vars(r)["typeID"]
|
||||||
|
if !ok {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "missing_type_id", Message: "Missing type ID", StatusCode: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := p.mm.User.Get(userID)
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := p.store.GetType(badgesmodel.BadgeType(typeID))
|
||||||
|
if err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "type_not_found", Message: "Type not found", StatusCode: http.StatusNotFound,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.IsDefault {
|
||||||
|
T := p.getT("ru")
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_delete_default_type", Message: T("badges.api.cannot_delete_default_type", "Нельзя удалить тип по умолчанию"), StatusCode: http.StatusForbidden,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canEditType(user, p.badgeAdminUserID, t) {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "no_permission", Message: "No permission to delete this type", StatusCode: http.StatusForbidden,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.store.DeleteType(badgesmodel.BadgeType(typeID)); err != nil {
|
||||||
|
p.writeAPIError(w, &APIErrorResponse{
|
||||||
|
ID: "cannot_delete_type", Message: err.Error(), StatusCode: http.StatusInternalServerError,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = w.Write([]byte(`{"success": true}`))
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Plugin) dialogCreateBadge(w http.ResponseWriter, r *http.Request, userID string) {
|
func (p *Plugin) dialogCreateBadge(w http.ResponseWriter, r *http.Request, userID string) {
|
||||||
req := model.SubmitDialogRequestFromJson(r.Body)
|
req := model.SubmitDialogRequestFromJson(r.Body)
|
||||||
if req == nil {
|
if req == nil {
|
||||||
@ -333,6 +729,10 @@ func (p *Plugin) dialogEditType(w http.ResponseWriter, r *http.Request, userID s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if getDialogSubmissionBoolField(req, DialogFieldTypeDelete) {
|
if getDialogSubmissionBoolField(req, DialogFieldTypeDelete) {
|
||||||
|
if originalType.IsDefault {
|
||||||
|
dialogError(w, T("badges.api.cannot_delete_default_type", "Нельзя удалить тип по умолчанию"), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
err = p.store.DeleteType(badgesmodel.BadgeType(originalTypeID))
|
err = p.store.DeleteType(badgesmodel.BadgeType(originalTypeID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dialogError(w, err.Error(), nil)
|
dialogError(w, err.Error(), nil)
|
||||||
|
|||||||
@ -96,6 +96,7 @@
|
|||||||
{"id": "badges.api.subscription_added", "translation": "Subscription added"},
|
{"id": "badges.api.subscription_added", "translation": "Subscription added"},
|
||||||
{"id": "badges.api.cannot_delete_subscription", "translation": "You cannot delete a subscription"},
|
{"id": "badges.api.cannot_delete_subscription", "translation": "You cannot delete a subscription"},
|
||||||
{"id": "badges.api.subscription_removed", "translation": "Subscription removed"},
|
{"id": "badges.api.subscription_removed", "translation": "Subscription removed"},
|
||||||
|
{"id": "badges.api.cannot_delete_default_type", "translation": "Cannot delete the default type"},
|
||||||
{"id": "badges.api.not_authorized", "translation": "Not authorized"},
|
{"id": "badges.api.not_authorized", "translation": "Not authorized"},
|
||||||
|
|
||||||
{"id": "badges.notify.dm_text", "translation": "@%s granted you the %s`%s` badge."},
|
{"id": "badges.notify.dm_text", "translation": "@%s granted you the %s`%s` badge."},
|
||||||
|
|||||||
@ -96,6 +96,7 @@
|
|||||||
{"id": "badges.api.subscription_added", "translation": "Подписка добавлена"},
|
{"id": "badges.api.subscription_added", "translation": "Подписка добавлена"},
|
||||||
{"id": "badges.api.cannot_delete_subscription", "translation": "Вы не можете удалить подписку"},
|
{"id": "badges.api.cannot_delete_subscription", "translation": "Вы не можете удалить подписку"},
|
||||||
{"id": "badges.api.subscription_removed", "translation": "Подписка удалена"},
|
{"id": "badges.api.subscription_removed", "translation": "Подписка удалена"},
|
||||||
|
{"id": "badges.api.cannot_delete_default_type", "translation": "Нельзя удалить тип по умолчанию"},
|
||||||
{"id": "badges.api.not_authorized", "translation": "Не авторизован"},
|
{"id": "badges.api.not_authorized", "translation": "Не авторизован"},
|
||||||
|
|
||||||
{"id": "badges.notify.dm_text", "translation": "@%s выдал вам значок %s`%s`."},
|
{"id": "badges.notify.dm_text", "translation": "@%s выдал вам значок %s`%s`."},
|
||||||
|
|||||||
@ -57,6 +57,9 @@ func (p *Plugin) OnActivate() error {
|
|||||||
}
|
}
|
||||||
p.BotUserID = botID
|
p.BotUserID = botID
|
||||||
p.store = NewStore(p.API)
|
p.store = NewStore(p.API)
|
||||||
|
if err := p.store.EnsureDefaultType(p.BotUserID); err != nil {
|
||||||
|
p.mm.Log.Warn("Failed to ensure default type", "error", err.Error())
|
||||||
|
}
|
||||||
p.i18nBundle = i18n.Init()
|
p.i18nBundle = i18n.Init()
|
||||||
p.initializeAPI()
|
p.initializeAPI()
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,9 @@ type Store interface {
|
|||||||
GetTypeSubscriptions(tID badgesmodel.BadgeType) ([]string, error)
|
GetTypeSubscriptions(tID badgesmodel.BadgeType) ([]string, error)
|
||||||
GetChannelSubscriptions(cID string) ([]*badgesmodel.BadgeTypeDefinition, error)
|
GetChannelSubscriptions(cID string) ([]*badgesmodel.BadgeTypeDefinition, error)
|
||||||
|
|
||||||
|
// Default type
|
||||||
|
EnsureDefaultType(botID string) error
|
||||||
|
|
||||||
// PAPI
|
// PAPI
|
||||||
EnsureBadges(badges []*badgesmodel.Badge, pluginID, botID string) ([]*badgesmodel.Badge, error)
|
EnsureBadges(badges []*badgesmodel.Badge, pluginID, botID string) ([]*badgesmodel.Badge, error)
|
||||||
}
|
}
|
||||||
@ -144,6 +147,28 @@ func (s *store) addType(t *badgesmodel.BadgeTypeDefinition, isPlugin bool) (*bad
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *store) EnsureDefaultType(botID string) error {
|
||||||
|
types, _, err := s.getAllTypes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range types {
|
||||||
|
if t.IsDefault {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.addType(&badgesmodel.BadgeTypeDefinition{
|
||||||
|
Name: badgesmodel.DefaultTypeName,
|
||||||
|
IsDefault: true,
|
||||||
|
CreatedBy: botID,
|
||||||
|
CanCreate: badgesmodel.PermissionScheme{Everyone: true},
|
||||||
|
CanGrant: badgesmodel.PermissionScheme{Everyone: true},
|
||||||
|
}, false)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *store) GetAllBadges() ([]*badgesmodel.AllBadgesBadge, error) {
|
func (s *store) GetAllBadges() ([]*badgesmodel.AllBadgesBadge, error) {
|
||||||
badges, _, err := s.getAllBadges()
|
badges, _, err := s.getAllBadges()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -417,6 +442,11 @@ func (s *store) atomicDeleteType(tID badgesmodel.BadgeType) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) DeleteType(tID badgesmodel.BadgeType) error {
|
func (s *store) DeleteType(tID badgesmodel.BadgeType) error {
|
||||||
|
t, err := s.GetType(tID)
|
||||||
|
if err == nil && t.IsDefault {
|
||||||
|
return errors.New("cannot delete default type")
|
||||||
|
}
|
||||||
|
|
||||||
s.doAtomic(func() (bool, error) { return s.atomicDeleteType(tID) })
|
s.doAtomic(func() (bool, error) { return s.atomicDeleteType(tID) })
|
||||||
|
|
||||||
bb, _, err := s.getAllBadges()
|
bb, _, err := s.getAllBadges()
|
||||||
|
|||||||
@ -697,7 +697,8 @@
|
|||||||
{
|
{
|
||||||
"extensions": [".jsx", ".tsx"]
|
"extensions": [".jsx", ".tsx"]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"react/prop-types": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"badges.loading": "Loading...",
|
"badges.loading": "Loading...",
|
||||||
"badges.no_badges_yet": "No badges yet.",
|
"badges.no_badges_yet": "No badges yet.",
|
||||||
|
"badges.empty.title": "No badges yet",
|
||||||
|
"badges.empty.description": "Create your first badge to recognize achievements and contributions of your team members.",
|
||||||
"badges.badge_not_found": "Badge not found.",
|
"badges.badge_not_found": "Badge not found.",
|
||||||
"badges.user_not_found": "User not found.",
|
"badges.user_not_found": "User not found.",
|
||||||
"badges.unknown": "unknown",
|
"badges.unknown": "unknown",
|
||||||
@ -10,6 +12,8 @@
|
|||||||
"badges.rhs.user_badges": "@{username}'s badges",
|
"badges.rhs.user_badges": "@{username}'s badges",
|
||||||
"badges.rhs.badge_details": "Badge Details",
|
"badges.rhs.badge_details": "Badge Details",
|
||||||
|
|
||||||
|
"badges.label.name": "Name:",
|
||||||
|
"badges.label.description": "Description:",
|
||||||
"badges.label.type": "Type: {typeName}",
|
"badges.label.type": "Type: {typeName}",
|
||||||
"badges.label.created_by": "Created by: {username}",
|
"badges.label.created_by": "Created by: {username}",
|
||||||
"badges.label.granted_by": "Granted by: {username}",
|
"badges.label.granted_by": "Granted by: {username}",
|
||||||
@ -20,6 +24,7 @@
|
|||||||
"badges.granted.multiple": "Granted {times, plural, one {# time} other {# times}} to {users, plural, one {# user} other {# users}}.",
|
"badges.granted.multiple": "Granted {times, plural, one {# time} other {# times}} to {users, plural, one {# user} other {# users}}.",
|
||||||
"badges.granted.single": "Granted to {users, plural, one {# user} other {# users}}.",
|
"badges.granted.single": "Granted to {users, plural, one {# user} other {# users}}.",
|
||||||
"badges.granted_to": "Granted to:",
|
"badges.granted_to": "Granted to:",
|
||||||
|
"badges.not_granted_yet": "Not granted to anyone yet",
|
||||||
|
|
||||||
"badges.set_status": "Set status to this badge",
|
"badges.set_status": "Set status to this badge",
|
||||||
"badges.grant_badge": "Grant badge",
|
"badges.grant_badge": "Grant badge",
|
||||||
@ -36,5 +41,56 @@
|
|||||||
|
|
||||||
"badges.admin.label": "Achievements Admin:",
|
"badges.admin.label": "Achievements Admin:",
|
||||||
"badges.admin.placeholder": "username",
|
"badges.admin.placeholder": "username",
|
||||||
"badges.admin.help_text": "This user will be considered the achievements plugin administrator. They can create types, as well as modify and grant any badges."
|
"badges.admin.help_text": "This user will be considered the achievements plugin administrator. They can create types, as well as modify and grant any badges.",
|
||||||
|
|
||||||
|
"badges.rhs.create_badge": "+ Create badge",
|
||||||
|
"badges.rhs.edit_badge": "Edit",
|
||||||
|
|
||||||
|
"badges.modal.create_badge_title": "Create Badge",
|
||||||
|
"badges.modal.edit_badge_title": "Edit Badge",
|
||||||
|
"badges.modal.field_name": "Name",
|
||||||
|
"badges.modal.field_name_placeholder": "Badge name (max 20 chars)",
|
||||||
|
"badges.modal.field_description": "Description",
|
||||||
|
"badges.modal.field_description_placeholder": "Badge description (max 120 chars)",
|
||||||
|
"badges.modal.field_image": "Emoji",
|
||||||
|
"badges.modal.field_image_placeholder": "Emoji name (e.g. star)",
|
||||||
|
"badges.modal.field_type": "Type",
|
||||||
|
"badges.modal.field_type_placeholder": "Select badge type",
|
||||||
|
"badges.modal.field_multiple": "Can be granted multiple times",
|
||||||
|
"badges.modal.create_new_type": "+ Create new type",
|
||||||
|
"badges.modal.new_type_name": "Type name",
|
||||||
|
"badges.modal.new_type_name_placeholder": "Type name (max 20 chars)",
|
||||||
|
"badges.modal.new_type_everyone_create": "Everyone can create badges",
|
||||||
|
"badges.modal.new_type_everyone_grant": "Everyone can grant badges",
|
||||||
|
"badges.modal.btn_cancel": "Cancel",
|
||||||
|
"badges.modal.btn_create": "Create",
|
||||||
|
"badges.modal.btn_save": "Save",
|
||||||
|
"badges.modal.btn_creating": "Saving...",
|
||||||
|
"badges.modal.btn_delete": "Delete badge",
|
||||||
|
"badges.modal.btn_confirm_delete": "Yes, delete",
|
||||||
|
"badges.modal.confirm_delete": "Are you sure?",
|
||||||
|
"badges.modal.error_generic": "An error occurred",
|
||||||
|
"badges.modal.error_type_name_required": "Enter type name",
|
||||||
|
"badges.modal.error_type_required": "Select badge type",
|
||||||
|
"badges.modal.delete_type": "Delete type",
|
||||||
|
"badges.modal.confirm_delete_type": "Delete type \"{name}\"?",
|
||||||
|
"badges.modal.btn_confirm_delete_type": "Yes, delete",
|
||||||
|
|
||||||
|
"badges.error.unknown": "An error occurred",
|
||||||
|
"badges.error.cannot_get_user": "Failed to get user data",
|
||||||
|
"badges.error.cannot_get_types": "Failed to load types",
|
||||||
|
"badges.error.cannot_get_badges": "Failed to load badges",
|
||||||
|
"badges.error.invalid_request": "Invalid request format",
|
||||||
|
"badges.error.invalid_name": "Name is required",
|
||||||
|
"badges.error.invalid_image": "Emoji is required",
|
||||||
|
"badges.error.type_not_found": "Badge type not found",
|
||||||
|
"badges.error.badge_not_found": "Badge not found",
|
||||||
|
"badges.error.no_permission": "Insufficient permissions",
|
||||||
|
"badges.error.missing_badge_id": "Badge ID is missing",
|
||||||
|
"badges.error.missing_type_id": "Type ID is missing",
|
||||||
|
"badges.error.cannot_create_badge": "Failed to create badge",
|
||||||
|
"badges.error.cannot_create_type": "Failed to create type",
|
||||||
|
"badges.error.cannot_update_badge": "Failed to update badge",
|
||||||
|
"badges.error.cannot_delete_badge": "Failed to delete badge",
|
||||||
|
"badges.error.cannot_delete_type": "Failed to delete type"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"badges.loading": "Загрузка...",
|
"badges.loading": "Загрузка...",
|
||||||
"badges.no_badges_yet": "Значков пока нет.",
|
"badges.no_badges_yet": "Значков пока нет.",
|
||||||
|
"badges.empty.title": "Значков пока нет",
|
||||||
|
"badges.empty.description": "Создайте первый значок, чтобы отмечать достижения и заслуги участников команды.",
|
||||||
"badges.badge_not_found": "Значок не найден.",
|
"badges.badge_not_found": "Значок не найден.",
|
||||||
"badges.user_not_found": "Пользователь не найден.",
|
"badges.user_not_found": "Пользователь не найден.",
|
||||||
"badges.unknown": "неизвестно",
|
"badges.unknown": "неизвестно",
|
||||||
@ -10,6 +12,8 @@
|
|||||||
"badges.rhs.user_badges": "Значки @{username}",
|
"badges.rhs.user_badges": "Значки @{username}",
|
||||||
"badges.rhs.badge_details": "Детали значка",
|
"badges.rhs.badge_details": "Детали значка",
|
||||||
|
|
||||||
|
"badges.label.name": "Название:",
|
||||||
|
"badges.label.description": "Описание:",
|
||||||
"badges.label.type": "Тип: {typeName}",
|
"badges.label.type": "Тип: {typeName}",
|
||||||
"badges.label.created_by": "Создал: {username}",
|
"badges.label.created_by": "Создал: {username}",
|
||||||
"badges.label.granted_by": "Выдал: {username}",
|
"badges.label.granted_by": "Выдал: {username}",
|
||||||
@ -20,6 +24,7 @@
|
|||||||
"badges.granted.multiple": "Выдан {times, plural, one {# раз} few {# раза} many {# раз} other {# раз}} {users, plural, one {# пользователю} few {# пользователям} many {# пользователям} other {# пользователям}}.",
|
"badges.granted.multiple": "Выдан {times, plural, one {# раз} few {# раза} many {# раз} other {# раз}} {users, plural, one {# пользователю} few {# пользователям} many {# пользователям} other {# пользователям}}.",
|
||||||
"badges.granted.single": "Выдан {users, plural, one {# пользователю} few {# пользователям} many {# пользователям} other {# пользователям}}.",
|
"badges.granted.single": "Выдан {users, plural, one {# пользователю} few {# пользователям} many {# пользователям} other {# пользователям}}.",
|
||||||
"badges.granted_to": "Выдан:",
|
"badges.granted_to": "Выдан:",
|
||||||
|
"badges.not_granted_yet": "Ещё никому не выдан",
|
||||||
|
|
||||||
"badges.set_status": "Установить как статус",
|
"badges.set_status": "Установить как статус",
|
||||||
"badges.grant_badge": "Выдать значок",
|
"badges.grant_badge": "Выдать значок",
|
||||||
@ -36,5 +41,56 @@
|
|||||||
|
|
||||||
"badges.admin.label": "Администратор достижений:",
|
"badges.admin.label": "Администратор достижений:",
|
||||||
"badges.admin.placeholder": "имя пользователя",
|
"badges.admin.placeholder": "имя пользователя",
|
||||||
"badges.admin.help_text": "Этот пользователь будет считаться администратором плагина достижений. Он может создавать типы, а также изменять и выдавать любые значки."
|
"badges.admin.help_text": "Этот пользователь будет считаться администратором плагина достижений. Он может создавать типы, а также изменять и выдавать любые значки.",
|
||||||
|
|
||||||
|
"badges.rhs.create_badge": "+ Создать значок",
|
||||||
|
"badges.rhs.edit_badge": "Редактировать",
|
||||||
|
|
||||||
|
"badges.modal.create_badge_title": "Создать значок",
|
||||||
|
"badges.modal.edit_badge_title": "Редактировать значок",
|
||||||
|
"badges.modal.field_name": "Название",
|
||||||
|
"badges.modal.field_name_placeholder": "Название значка (макс. 20 символов)",
|
||||||
|
"badges.modal.field_description": "Описание",
|
||||||
|
"badges.modal.field_description_placeholder": "Описание значка (макс. 120 символов)",
|
||||||
|
"badges.modal.field_image": "Эмодзи",
|
||||||
|
"badges.modal.field_image_placeholder": "Название эмодзи (напр. star)",
|
||||||
|
"badges.modal.field_type": "Тип",
|
||||||
|
"badges.modal.field_type_placeholder": "Выберите тип значка",
|
||||||
|
"badges.modal.field_multiple": "Можно выдавать несколько раз",
|
||||||
|
"badges.modal.create_new_type": "+ Создать новый тип",
|
||||||
|
"badges.modal.new_type_name": "Название типа",
|
||||||
|
"badges.modal.new_type_name_placeholder": "Название типа (макс. 20 символов)",
|
||||||
|
"badges.modal.new_type_everyone_create": "Все могут создавать значки",
|
||||||
|
"badges.modal.new_type_everyone_grant": "Все могут выдавать значки",
|
||||||
|
"badges.modal.btn_cancel": "Отмена",
|
||||||
|
"badges.modal.btn_create": "Создать",
|
||||||
|
"badges.modal.btn_save": "Сохранить",
|
||||||
|
"badges.modal.btn_creating": "Сохранение...",
|
||||||
|
"badges.modal.btn_delete": "Удалить значок",
|
||||||
|
"badges.modal.btn_confirm_delete": "Да, удалить",
|
||||||
|
"badges.modal.confirm_delete": "Вы уверены?",
|
||||||
|
"badges.modal.error_generic": "Произошла ошибка",
|
||||||
|
"badges.modal.error_type_name_required": "Введите название типа",
|
||||||
|
"badges.modal.error_type_required": "Выберите тип значка",
|
||||||
|
"badges.modal.delete_type": "Удалить тип",
|
||||||
|
"badges.modal.confirm_delete_type": "Удалить тип «{name}»?",
|
||||||
|
"badges.modal.btn_confirm_delete_type": "Да, удалить",
|
||||||
|
|
||||||
|
"badges.error.unknown": "Произошла ошибка",
|
||||||
|
"badges.error.cannot_get_user": "Не удалось получить данные пользователя",
|
||||||
|
"badges.error.cannot_get_types": "Не удалось загрузить типы",
|
||||||
|
"badges.error.cannot_get_badges": "Не удалось загрузить значки",
|
||||||
|
"badges.error.invalid_request": "Неверный формат запроса",
|
||||||
|
"badges.error.invalid_name": "Необходимо указать название",
|
||||||
|
"badges.error.invalid_image": "Необходимо указать эмодзи",
|
||||||
|
"badges.error.type_not_found": "Тип значка не найден",
|
||||||
|
"badges.error.badge_not_found": "Значок не найден",
|
||||||
|
"badges.error.no_permission": "Недостаточно прав для выполнения действия",
|
||||||
|
"badges.error.missing_badge_id": "Не указан ID значка",
|
||||||
|
"badges.error.missing_type_id": "Не указан ID типа",
|
||||||
|
"badges.error.cannot_create_badge": "Не удалось создать значок",
|
||||||
|
"badges.error.cannot_create_type": "Не удалось создать тип",
|
||||||
|
"badges.error.cannot_update_badge": "Не удалось обновить значок",
|
||||||
|
"badges.error.cannot_delete_badge": "Не удалось удалить значок",
|
||||||
|
"badges.error.cannot_delete_type": "Не удалось удалить тип"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,7 +57,6 @@
|
|||||||
"jest": "26.6.3",
|
"jest": "26.6.3",
|
||||||
"jest-canvas-mock": "2.3.1",
|
"jest-canvas-mock": "2.3.1",
|
||||||
"jest-junit": "12.0.0",
|
"jest-junit": "12.0.0",
|
||||||
"loop-plugin-sdk": "https://artifacts.wilix.dev/repository/npm-public-loop/loop-plugin-sdk/-/loop-plugin-sdk-0.1.6.tgz",
|
|
||||||
"react-intl": "6.8.9",
|
"react-intl": "6.8.9",
|
||||||
"sass": "1.86.0",
|
"sass": "1.86.0",
|
||||||
"sass-loader": "11.0.1",
|
"sass-loader": "11.0.1",
|
||||||
|
|||||||
@ -8,4 +8,8 @@ export default {
|
|||||||
RECEIVED_RHS_VIEW: pluginId + '_received_rhs_view',
|
RECEIVED_RHS_VIEW: pluginId + '_received_rhs_view',
|
||||||
RECEIVED_RHS_USER: pluginId + '_received_rhs_user',
|
RECEIVED_RHS_USER: pluginId + '_received_rhs_user',
|
||||||
RECEIVED_RHS_BADGE: pluginId + '_received_rhs_badge',
|
RECEIVED_RHS_BADGE: pluginId + '_received_rhs_badge',
|
||||||
|
OPEN_CREATE_BADGE_MODAL: pluginId + '_open_create_badge_modal',
|
||||||
|
CLOSE_CREATE_BADGE_MODAL: pluginId + '_close_create_badge_modal',
|
||||||
|
OPEN_EDIT_BADGE_MODAL: pluginId + '_open_edit_badge_modal',
|
||||||
|
CLOSE_EDIT_BADGE_MODAL: pluginId + '_close_edit_badge_modal',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {Client4} from 'mattermost-redux/client';
|
|||||||
import {IntegrationTypes} from 'mattermost-redux/action_types';
|
import {IntegrationTypes} from 'mattermost-redux/action_types';
|
||||||
|
|
||||||
import ActionTypes from 'action_types/';
|
import ActionTypes from 'action_types/';
|
||||||
import {BadgeID} from 'types/badges';
|
import {BadgeDetails, BadgeID} from 'types/badges';
|
||||||
import {RHSState} from 'types/general';
|
import {RHSState} from 'types/general';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,14 +76,28 @@ export function openCreateType() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function openCreateBadge() {
|
export function openCreateBadge() {
|
||||||
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
return (dispatch: Dispatch<AnyAction>) => {
|
||||||
const command = '/badges create badge';
|
dispatch(openCreateBadgeModal());
|
||||||
clientExecuteCommand(dispatch, getState, command);
|
|
||||||
|
|
||||||
return {data: true};
|
return {data: true};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openCreateBadgeModal() {
|
||||||
|
return {type: ActionTypes.OPEN_CREATE_BADGE_MODAL};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeCreateBadgeModal() {
|
||||||
|
return {type: ActionTypes.CLOSE_CREATE_BADGE_MODAL};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openEditBadgeModal(badge: BadgeDetails) {
|
||||||
|
return {type: ActionTypes.OPEN_EDIT_BADGE_MODAL, data: badge};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeEditBadgeModal() {
|
||||||
|
return {type: ActionTypes.CLOSE_EDIT_BADGE_MODAL};
|
||||||
|
}
|
||||||
|
|
||||||
export function openAddSubscription() {
|
export function openAddSubscription() {
|
||||||
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
||||||
const command = '/badges subscription create';
|
const command = '/badges subscription create';
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {Client4} from 'mattermost-redux/client';
|
|||||||
import {ClientError} from 'mattermost-redux/client/client4';
|
import {ClientError} from 'mattermost-redux/client/client4';
|
||||||
|
|
||||||
import manifest from 'manifest';
|
import manifest from 'manifest';
|
||||||
import {AllBadgesBadge, BadgeDetails, BadgeID, UserBadge} from 'types/badges';
|
import {AllBadgesBadge, Badge, BadgeDetails, BadgeID, BadgeTypeDefinition, CreateBadgeRequest, CreateTypeRequest, GetTypesResponse, UpdateBadgeRequest, UserBadge} from 'types/badges';
|
||||||
|
|
||||||
export default class Client {
|
export default class Client {
|
||||||
private url: string;
|
private url: string;
|
||||||
@ -41,6 +41,35 @@ export default class Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTypes(): Promise<GetTypesResponse> {
|
||||||
|
try {
|
||||||
|
const res = await this.doGet(`${this.url}/getTypes`);
|
||||||
|
return res as GetTypesResponse;
|
||||||
|
} catch {
|
||||||
|
return {types: [], can_create_type: false};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBadge(req: CreateBadgeRequest): Promise<Badge> {
|
||||||
|
return await this.doPost(`${this.url}/createBadge`, req) as Badge;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createType(req: CreateTypeRequest): Promise<BadgeTypeDefinition> {
|
||||||
|
return await this.doPost(`${this.url}/createType`, req) as BadgeTypeDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBadge(req: UpdateBadgeRequest): Promise<Badge> {
|
||||||
|
return await this.doPut(`${this.url}/updateBadge`, req) as Badge;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteBadge(badgeID: BadgeID): Promise<void> {
|
||||||
|
await this.doDelete(`${this.url}/deleteBadge/${badgeID}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteType(typeID: string): Promise<void> {
|
||||||
|
await this.doDelete(`${this.url}/deleteType/${typeID}`);
|
||||||
|
}
|
||||||
|
|
||||||
private doGet = async (url: string, headers: {[x:string]: string} = {}) => {
|
private doGet = async (url: string, headers: {[x:string]: string} = {}) => {
|
||||||
headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset());
|
headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset());
|
||||||
|
|
||||||
@ -63,4 +92,75 @@ export default class Client {
|
|||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private doPost = async (url: string, body: any, headers: {[x:string]: string} = {}) => {
|
||||||
|
headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset());
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: 'post',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, Client4.getOptions(options));
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
throw new ClientError(Client4.url, {
|
||||||
|
message: text || '',
|
||||||
|
status_code: response.status,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private doPut = async (url: string, body: any, headers: {[x:string]: string} = {}) => {
|
||||||
|
headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset());
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: 'put',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, Client4.getOptions(options));
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
throw new ClientError(Client4.url, {
|
||||||
|
message: text || '',
|
||||||
|
status_code: response.status,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private doDelete = async (url: string, headers: {[x:string]: string} = {}) => {
|
||||||
|
headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset());
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: 'delete',
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, Client4.getOptions(options));
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
throw new ClientError(Client4.url, {
|
||||||
|
message: text || '',
|
||||||
|
status_code: response.status,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
297
webapp/src/components/badge_modal/badge_modal.scss
Normal file
297
webapp/src/components/badge_modal/badge_modal.scss
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
.BadgeModal {
|
||||||
|
&__backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 10001;
|
||||||
|
background: var(--center-channel-bg, #fff);
|
||||||
|
color: var(--center-channel-color, #3d3c40);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 20px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
width: 480px;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 24px 0;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--center-channel-color, #3d3c40);
|
||||||
|
opacity: 0.56;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
padding: 20px 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.64;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.16);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--center-channel-bg, #fff);
|
||||||
|
color: var(--center-channel-color, #3d3c40);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--button-bg, #166de0);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
text-transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-type-section {
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border: 1px dashed rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.24);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background: var(--button-bg, #166de0);
|
||||||
|
color: var(--button-color, #fff);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--cancel {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--center-channel-color, #3d3c40);
|
||||||
|
border: 1px solid rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.16);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
background: var(--error-text, #d24b4e);
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: var(--error-text, #d24b4e);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.08);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-delete {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--error-text, #d24b4e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-select {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__trigger {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.16);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--center-channel-bg, #fff);
|
||||||
|
color: var(--center-channel-color, #3d3c40);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.56;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: var(--center-channel-bg, #fff);
|
||||||
|
border: 1px solid rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.16);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||||
|
z-index: 10;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
background: rgba(var(--button-bg-rgb, 22, 109, 224), 0.08);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--create {
|
||||||
|
border-top: 1px solid rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.08);
|
||||||
|
color: var(--button-bg, #166de0);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__delete-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--center-channel-color, #3d3c40);
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--error-text, #d24b4e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
416
webapp/src/components/badge_modal/index.tsx
Normal file
416
webapp/src/components/badge_modal/index.tsx
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
import React, {useCallback, useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
|
||||||
|
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/common';
|
||||||
|
import {GlobalState} from 'mattermost-redux/types/store';
|
||||||
|
|
||||||
|
import {isCreateBadgeModalVisible, getEditBadgeModalData} from 'selectors';
|
||||||
|
import {closeCreateBadgeModal, closeEditBadgeModal} from 'actions/actions';
|
||||||
|
import {BadgeTypeDefinition} from 'types/badges';
|
||||||
|
import Client from 'client/api';
|
||||||
|
import {getServerErrorId} from 'utils/helpers';
|
||||||
|
|
||||||
|
import TypeSelect from './type_select';
|
||||||
|
|
||||||
|
import './badge_modal.scss';
|
||||||
|
|
||||||
|
const NEW_TYPE_VALUE = '__new__';
|
||||||
|
|
||||||
|
const BadgeModal: React.FC = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
const createVisible = useSelector(isCreateBadgeModalVisible);
|
||||||
|
const editData = useSelector(getEditBadgeModalData);
|
||||||
|
const channelId = useSelector((state: GlobalState) => getCurrentChannelId(state));
|
||||||
|
const isOpen = createVisible || editData !== null;
|
||||||
|
const isEditMode = editData !== null;
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [image, setImage] = useState('');
|
||||||
|
const [badgeType, setBadgeType] = useState('');
|
||||||
|
const [multiple, setMultiple] = useState(false);
|
||||||
|
const [types, setTypes] = useState<BadgeTypeDefinition[]>([]);
|
||||||
|
const [showCreateType, setShowCreateType] = useState(false);
|
||||||
|
const [newTypeName, setNewTypeName] = useState('');
|
||||||
|
const [newTypeEveryoneCanCreate, setNewTypeEveryoneCanCreate] = useState(false);
|
||||||
|
const [newTypeEveryoneCanGrant, setNewTypeEveryoneCanGrant] = useState(false);
|
||||||
|
const [canCreateType, setCanCreateType] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
|
const [confirmDeleteTypeId, setConfirmDeleteTypeId] = useState<string | null>(null);
|
||||||
|
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fetchTypes = async () => {
|
||||||
|
const client = new Client();
|
||||||
|
const resp = await client.getTypes();
|
||||||
|
setTypes(resp.types);
|
||||||
|
setCanCreateType(resp.can_create_type);
|
||||||
|
if (!isEditMode && resp.types.length > 0) {
|
||||||
|
const defaultType = resp.types.find((t) => t.is_default);
|
||||||
|
setBadgeType(String(defaultType ? defaultType.id : resp.types[0].id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchTypes();
|
||||||
|
|
||||||
|
if (isEditMode && editData) {
|
||||||
|
setName(editData.name);
|
||||||
|
setDescription(editData.description);
|
||||||
|
setImage(editData.image);
|
||||||
|
setBadgeType(String(editData.type));
|
||||||
|
setMultiple(editData.multiple);
|
||||||
|
} else {
|
||||||
|
setName('');
|
||||||
|
setDescription('');
|
||||||
|
setImage('');
|
||||||
|
setBadgeType('');
|
||||||
|
setMultiple(false);
|
||||||
|
}
|
||||||
|
setShowCreateType(false);
|
||||||
|
setNewTypeName('');
|
||||||
|
setNewTypeEveryoneCanCreate(false);
|
||||||
|
setNewTypeEveryoneCanGrant(false);
|
||||||
|
setError(null);
|
||||||
|
setConfirmDelete(false);
|
||||||
|
setConfirmDeleteTypeId(null);
|
||||||
|
setTypeDropdownOpen(false);
|
||||||
|
setLoading(false);
|
||||||
|
}, [isOpen, isEditMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
if (createVisible) {
|
||||||
|
dispatch(closeCreateBadgeModal());
|
||||||
|
}
|
||||||
|
if (editData) {
|
||||||
|
dispatch(closeEditBadgeModal());
|
||||||
|
}
|
||||||
|
}, [dispatch, createVisible, editData]);
|
||||||
|
|
||||||
|
const handleTypeSelect = useCallback((val: string) => {
|
||||||
|
if (val === NEW_TYPE_VALUE) {
|
||||||
|
setShowCreateType(true);
|
||||||
|
setBadgeType('');
|
||||||
|
} else {
|
||||||
|
setShowCreateType(false);
|
||||||
|
setBadgeType(val);
|
||||||
|
}
|
||||||
|
setTypeDropdownOpen(false);
|
||||||
|
setConfirmDeleteTypeId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteType = useCallback(async (typeId: string) => {
|
||||||
|
if (confirmDeleteTypeId !== typeId) {
|
||||||
|
setConfirmDeleteTypeId(typeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const client = new Client();
|
||||||
|
await client.deleteType(typeId);
|
||||||
|
const removeById = (t: BadgeTypeDefinition) => String(t.id) !== typeId;
|
||||||
|
setTypes((prev) => prev.filter(removeById));
|
||||||
|
if (badgeType === typeId) {
|
||||||
|
setBadgeType('');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(intl.formatMessage({id: 'badges.error.' + (getServerErrorId(err) || 'unknown'), defaultMessage: 'Произошла ошибка'}));
|
||||||
|
}
|
||||||
|
setConfirmDeleteTypeId(null);
|
||||||
|
}, [confirmDeleteTypeId, badgeType, intl]);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const client = new Client();
|
||||||
|
let typeID = badgeType;
|
||||||
|
if (showCreateType) {
|
||||||
|
if (!newTypeName.trim()) {
|
||||||
|
setError(intl.formatMessage({id: 'badges.modal.error_type_name_required', defaultMessage: 'Введите название типа'}));
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const createdType = await client.createType({
|
||||||
|
name: newTypeName.trim(),
|
||||||
|
everyone_can_create: newTypeEveryoneCanCreate,
|
||||||
|
everyone_can_grant: newTypeEveryoneCanGrant,
|
||||||
|
channel_id: channelId,
|
||||||
|
});
|
||||||
|
typeID = String(createdType.id);
|
||||||
|
}
|
||||||
|
if (!typeID) {
|
||||||
|
setError(intl.formatMessage({id: 'badges.modal.error_type_required', defaultMessage: 'Выберите тип значка'}));
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isEditMode && editData) {
|
||||||
|
await client.updateBadge({
|
||||||
|
id: String(editData.id),
|
||||||
|
name: name.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
image: image.trim(),
|
||||||
|
type: typeID,
|
||||||
|
multiple,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await client.createBadge({
|
||||||
|
name: name.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
image: image.trim(),
|
||||||
|
type: typeID,
|
||||||
|
multiple,
|
||||||
|
channel_id: channelId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleClose();
|
||||||
|
} catch (err) {
|
||||||
|
setError(intl.formatMessage({id: 'badges.error.' + (getServerErrorId(err) || 'unknown'), defaultMessage: 'Произошла ошибка'}));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [badgeType, showCreateType, newTypeName, newTypeEveryoneCanCreate, newTypeEveryoneCanGrant, isEditMode, editData, name, description, image, multiple, handleClose, intl, channelId]);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(async () => {
|
||||||
|
if (!editData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirmDelete) {
|
||||||
|
setConfirmDelete(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const client = new Client();
|
||||||
|
await client.deleteBadge(editData.id);
|
||||||
|
handleClose();
|
||||||
|
} catch (err) {
|
||||||
|
setError(intl.formatMessage({id: 'badges.error.' + (getServerErrorId(err) || 'unknown'), defaultMessage: 'Произошла ошибка'}));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [editData, confirmDelete, handleClose, intl]);
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = isEditMode ? intl.formatMessage({id: 'badges.modal.edit_badge_title', defaultMessage: 'Редактировать значок'}) : intl.formatMessage({id: 'badges.modal.create_badge_title', defaultMessage: 'Создать значок'});
|
||||||
|
const submitLabel = isEditMode ? intl.formatMessage({id: 'badges.modal.btn_save', defaultMessage: 'Сохранить'}) : intl.formatMessage({id: 'badges.modal.btn_create', defaultMessage: 'Создать'});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='BadgeModal'>
|
||||||
|
<div
|
||||||
|
className='BadgeModal__backdrop'
|
||||||
|
onClick={handleClose}
|
||||||
|
/>
|
||||||
|
<div className='BadgeModal__dialog'>
|
||||||
|
<div className='BadgeModal__header'>
|
||||||
|
<h4>{title}</h4>
|
||||||
|
<button
|
||||||
|
className='close-btn'
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
{'\u00D7'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='BadgeModal__body'>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.field_name'
|
||||||
|
defaultMessage='Название'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
maxLength={20}
|
||||||
|
placeholder={intl.formatMessage({id: 'badges.modal.field_name_placeholder', defaultMessage: 'Название значка (макс. 20 символов)'})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.field_description'
|
||||||
|
defaultMessage='Описание'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
maxLength={120}
|
||||||
|
placeholder={intl.formatMessage({id: 'badges.modal.field_description_placeholder', defaultMessage: 'Описание значка (макс. 120 символов)'})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.field_image'
|
||||||
|
defaultMessage='Эмодзи'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={image}
|
||||||
|
onChange={(e) => setImage(e.target.value)}
|
||||||
|
placeholder={intl.formatMessage({id: 'badges.modal.field_image_placeholder', defaultMessage: 'Название эмодзи (напр. star)'})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.field_type'
|
||||||
|
defaultMessage='Тип'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<TypeSelect
|
||||||
|
types={types}
|
||||||
|
badgeType={badgeType}
|
||||||
|
showCreateType={showCreateType}
|
||||||
|
canCreateType={canCreateType}
|
||||||
|
typeDropdownOpen={typeDropdownOpen}
|
||||||
|
confirmDeleteTypeId={confirmDeleteTypeId}
|
||||||
|
onToggleDropdown={() => setTypeDropdownOpen(!typeDropdownOpen)}
|
||||||
|
onSelect={handleTypeSelect}
|
||||||
|
onDeleteType={handleDeleteType}
|
||||||
|
onCancelDeleteType={() => setConfirmDeleteTypeId(null)}
|
||||||
|
/>
|
||||||
|
{showCreateType && (
|
||||||
|
<div className='inline-type-section'>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.new_type_name'
|
||||||
|
defaultMessage='Название типа'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={newTypeName}
|
||||||
|
onChange={(e) => setNewTypeName(e.target.value)}
|
||||||
|
maxLength={20}
|
||||||
|
placeholder={intl.formatMessage({id: 'badges.modal.new_type_name_placeholder', defaultMessage: 'Название типа (макс. 20 символов)'})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='checkbox-group'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
id='newTypeEveryoneCanCreate'
|
||||||
|
checked={newTypeEveryoneCanCreate}
|
||||||
|
onChange={(e) => setNewTypeEveryoneCanCreate(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor='newTypeEveryoneCanCreate'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.new_type_everyone_create'
|
||||||
|
defaultMessage='Все могут создавать значки'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='checkbox-group'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
id='newTypeEveryoneCanGrant'
|
||||||
|
checked={newTypeEveryoneCanGrant}
|
||||||
|
onChange={(e) => setNewTypeEveryoneCanGrant(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor='newTypeEveryoneCanGrant'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.new_type_everyone_grant'
|
||||||
|
defaultMessage='Все могут выдавать значки'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='checkbox-group'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
id='badgeMultiple'
|
||||||
|
checked={multiple}
|
||||||
|
onChange={(e) => setMultiple(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor='badgeMultiple'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.field_multiple'
|
||||||
|
defaultMessage='Можно выдавать несколько раз'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{error && <div className='error-message'>{error}</div>}
|
||||||
|
{isEditMode && (
|
||||||
|
<div className='delete-section'>
|
||||||
|
{confirmDelete ? (
|
||||||
|
<div className='confirm-delete'>
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.confirm_delete'
|
||||||
|
defaultMessage='Вы уверены?'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className='btn btn--danger'
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.btn_confirm_delete'
|
||||||
|
defaultMessage='Да, удалить'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='btn btn--cancel'
|
||||||
|
onClick={() => setConfirmDelete(false)}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.btn_cancel'
|
||||||
|
defaultMessage='Отмена'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className='btn btn--danger'
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.btn_delete'
|
||||||
|
defaultMessage='Удалить значок'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='BadgeModal__footer'>
|
||||||
|
<button
|
||||||
|
className='btn btn--cancel'
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.btn_cancel'
|
||||||
|
defaultMessage='Отмена'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='btn btn--primary'
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={loading || !name.trim() || !image.trim()}
|
||||||
|
>
|
||||||
|
{loading ? intl.formatMessage({id: 'badges.modal.btn_creating', defaultMessage: 'Сохранение...'}) : submitLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BadgeModal;
|
||||||
110
webapp/src/components/badge_modal/type_select.tsx
Normal file
110
webapp/src/components/badge_modal/type_select.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
|
||||||
|
import {BadgeTypeDefinition} from 'types/badges';
|
||||||
|
import TrashIcon from 'components/icons/trash_icon';
|
||||||
|
import ConfirmDialog from 'components/confirm_dialog/confirm_dialog';
|
||||||
|
|
||||||
|
const NEW_TYPE_VALUE = '__new__';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
types: BadgeTypeDefinition[];
|
||||||
|
badgeType: string;
|
||||||
|
showCreateType: boolean;
|
||||||
|
canCreateType: boolean;
|
||||||
|
typeDropdownOpen: boolean;
|
||||||
|
confirmDeleteTypeId: string | null;
|
||||||
|
onToggleDropdown: () => void;
|
||||||
|
onSelect: (val: string) => void;
|
||||||
|
onDeleteType: (typeId: string) => void;
|
||||||
|
onCancelDeleteType: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TypeSelect: React.FC<Props> = ({
|
||||||
|
types,
|
||||||
|
badgeType,
|
||||||
|
showCreateType,
|
||||||
|
canCreateType,
|
||||||
|
typeDropdownOpen,
|
||||||
|
confirmDeleteTypeId,
|
||||||
|
onToggleDropdown,
|
||||||
|
onSelect,
|
||||||
|
onDeleteType,
|
||||||
|
onCancelDeleteType,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const selectedTypeName = types.find((t) => String(t.id) === badgeType)?.name ||
|
||||||
|
intl.formatMessage({id: 'badges.modal.field_type_placeholder', defaultMessage: 'Выберите тип значка'});
|
||||||
|
const triggerLabel = showCreateType ? intl.formatMessage({id: 'badges.modal.create_new_type', defaultMessage: '+ Создать новый тип'}) : selectedTypeName;
|
||||||
|
const confirmType = confirmDeleteTypeId ? types.find((t) => String(t.id) === confirmDeleteTypeId) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='type-select'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='type-select__trigger'
|
||||||
|
onClick={onToggleDropdown}
|
||||||
|
>
|
||||||
|
<span className='type-select__value'>{triggerLabel}</span>
|
||||||
|
<span className='type-select__arrow'>{'\u25BE'}</span>
|
||||||
|
</button>
|
||||||
|
{typeDropdownOpen && (
|
||||||
|
<div className='type-select__dropdown'>
|
||||||
|
{types.map((t) => {
|
||||||
|
const tid = String(t.id);
|
||||||
|
const isEmpty = t.badge_count === 0;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={tid}
|
||||||
|
className={'type-select__option' + (tid === badgeType ? ' type-select__option--selected' : '')}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className='type-select__option-name'
|
||||||
|
onClick={() => onSelect(tid)}
|
||||||
|
>
|
||||||
|
{t.name}
|
||||||
|
</span>
|
||||||
|
{isEmpty && !t.is_default && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='type-select__delete-btn'
|
||||||
|
onClick={() => onDeleteType(tid)}
|
||||||
|
title={intl.formatMessage({id: 'badges.modal.delete_type', defaultMessage: 'Удалить тип'})}
|
||||||
|
>
|
||||||
|
<TrashIcon/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{canCreateType && (
|
||||||
|
<div
|
||||||
|
className='type-select__option type-select__option--create'
|
||||||
|
onClick={() => onSelect(NEW_TYPE_VALUE)}
|
||||||
|
>
|
||||||
|
<span className='type-select__option-name'>
|
||||||
|
{intl.formatMessage({id: 'badges.modal.create_new_type', defaultMessage: '+ Создать новый тип'})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{confirmType && (
|
||||||
|
<ConfirmDialog
|
||||||
|
onConfirm={() => onDeleteType(String(confirmDeleteTypeId))}
|
||||||
|
onCancel={onCancelDeleteType}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.confirm_delete_type'
|
||||||
|
defaultMessage='Удалить тип «{name}»?'
|
||||||
|
values={{name: confirmType.name}}
|
||||||
|
/>
|
||||||
|
</ConfirmDialog>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TypeSelect;
|
||||||
34
webapp/src/components/confirm_dialog/confirm_dialog.scss
Normal file
34
webapp/src/components/confirm_dialog/confirm_dialog.scss
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
.ConfirmDialog {
|
||||||
|
background: var(--center-channel-bg, #fff);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
|
||||||
|
min-width: 240px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 11;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--center-channel-color, #3d3c40);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
webapp/src/components/confirm_dialog/confirm_dialog.tsx
Normal file
45
webapp/src/components/confirm_dialog/confirm_dialog.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import './confirm_dialog.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfirmDialog: React.FC<Props> = ({children, onConfirm, onCancel}) => (
|
||||||
|
<div className='ConfirmDialog__overlay'>
|
||||||
|
<div className='ConfirmDialog'>
|
||||||
|
<p className='ConfirmDialog__text'>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
<div className='ConfirmDialog__actions'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn--cancel'
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.btn_cancel'
|
||||||
|
defaultMessage='Отмена'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn--danger'
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.modal.btn_confirm_delete'
|
||||||
|
defaultMessage='Да, удалить'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ConfirmDialog;
|
||||||
27
webapp/src/components/icons/trash_icon.tsx
Normal file
27
webapp/src/components/icons/trash_icon.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TrashIcon: React.FC<Props> = ({size = 16}) => (
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth='2'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
>
|
||||||
|
<path d='M4 7l16 0'/>
|
||||||
|
<path d='M10 11l0 6'/>
|
||||||
|
<path d='M14 11l0 6'/>
|
||||||
|
<path d='M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12'/>
|
||||||
|
<path d='M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3'/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TrashIcon;
|
||||||
@ -3,4 +3,63 @@
|
|||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--empty {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__emptyContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__emptyTitle {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--center-channel-color, #3d3c40);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__emptyDescription {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.72);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__createButton {
|
||||||
|
background: var(--button-bg, #166de0);
|
||||||
|
color: var(--button-color, #fff);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ type Props = {
|
|||||||
setRHSView: (view: RHSState) => void;
|
setRHSView: (view: RHSState) => void;
|
||||||
setRHSBadge: (badge: BadgeID | null) => void;
|
setRHSBadge: (badge: BadgeID | null) => void;
|
||||||
getCustomEmojisByName: (names: string[]) => void;
|
getCustomEmojisByName: (names: string[]) => void;
|
||||||
|
openCreateBadgeModal: () => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,20 +65,36 @@ class AllBadges extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
return (<div className='AllBadges'>
|
return (<div className='AllBadges AllBadges--loading'>
|
||||||
<FormattedMessage
|
<div className='spinner'/>
|
||||||
id='badges.loading'
|
|
||||||
defaultMessage='Загрузка...'
|
|
||||||
/>
|
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.badges || this.state.badges.length === 0) {
|
if (!this.state.badges || this.state.badges.length === 0) {
|
||||||
return (<div className='AllBadges'>
|
return (<div className='AllBadges AllBadges--empty'>
|
||||||
|
<div className='AllBadges__emptyContent'>
|
||||||
|
<div className='AllBadges__emptyTitle'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.no_badges_yet'
|
id='badges.empty.title'
|
||||||
defaultMessage='Значков пока нет.'
|
defaultMessage='Значков пока нет'
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='AllBadges__emptyDescription'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.empty.description'
|
||||||
|
defaultMessage='Создайте первый значок, чтобы отмечать достижения и заслуги участников команды.'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className='AllBadges__createButton'
|
||||||
|
onClick={this.props.actions.openCreateBadgeModal}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.rhs.create_badge'
|
||||||
|
defaultMessage='+ Создать значок'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,12 +109,23 @@ class AllBadges extends React.PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div className='AllBadges'>
|
<div className='AllBadges'>
|
||||||
<div><b>
|
<div className='AllBadges__header'>
|
||||||
|
<b>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.rhs.all_badges'
|
id='badges.rhs.all_badges'
|
||||||
defaultMessage='Все значки'
|
defaultMessage='Все значки'
|
||||||
/>
|
/>
|
||||||
</b></div>
|
</b>
|
||||||
|
<button
|
||||||
|
className='AllBadges__createButton'
|
||||||
|
onClick={this.props.actions.openCreateBadgeModal}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.rhs.create_badge'
|
||||||
|
defaultMessage='+ Создать значок'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<RHSScrollbars>{content}</RHSScrollbars>
|
<RHSScrollbars>{content}</RHSScrollbars>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,25 +1,59 @@
|
|||||||
.AllBadgesRow {
|
.AllBadgesRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
|
||||||
border-radius: 4px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px;
|
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||||
margin-bottom: 3px;
|
border-radius: 6px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 12px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(var(--center-channel-color-rgb), 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
.badge-icon {
|
.badge-icon {
|
||||||
padding: 10px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.badge-name {
|
.badge-name {
|
||||||
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: var(--center-channel-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.granted-by {
|
|
||||||
font-size: 10px;
|
.badge-description {
|
||||||
}
|
font-size: 13px;
|
||||||
.badge-type {
|
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||||
font-size: 10px;
|
margin-top: 2px;
|
||||||
}
|
|
||||||
.badge-descrition {
|
|
||||||
p {
|
p {
|
||||||
margin: 0px
|
margin: 0;
|
||||||
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-label {
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||||
|
margin-top: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,29 +43,46 @@ function getGrantedText(badge: AllBadgesBadge): React.ReactNode {
|
|||||||
|
|
||||||
const AllBadgesRow: React.FC<Props> = ({badge, onClick}: Props) => {
|
const AllBadgesRow: React.FC<Props> = ({badge, onClick}: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className='AllBadgesRow'>
|
<div
|
||||||
<a
|
className='AllBadgesRow'
|
||||||
className='badge-icon'
|
|
||||||
onClick={() => onClick(badge)}
|
onClick={() => onClick(badge)}
|
||||||
>
|
>
|
||||||
<span>
|
<span className='badge-icon'>
|
||||||
<BadgeImage
|
<BadgeImage
|
||||||
badge={badge}
|
badge={badge}
|
||||||
size={32}
|
size={36}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
<div className='badge-text'>
|
||||||
<div>
|
<div className='badge-name'>
|
||||||
<div className='badge-name'>{badge.name}</div>
|
<span className='badge-label'>
|
||||||
<div className='badge-description'>{markdown(badge.description)}</div>
|
<FormattedMessage
|
||||||
<div className='badge-type'>
|
id='badges.label.name'
|
||||||
|
defaultMessage='Название:'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{' '}
|
||||||
|
{badge.name}
|
||||||
|
</div>
|
||||||
|
<div className='badge-description'>
|
||||||
|
<span className='badge-label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.label.description'
|
||||||
|
defaultMessage='Описание:'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{' '}
|
||||||
|
{markdown(badge.description)}
|
||||||
|
</div>
|
||||||
|
<div className='badge-meta'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.label.type'
|
id='badges.label.type'
|
||||||
defaultMessage='Тип: {typeName}'
|
defaultMessage='Тип: {typeName}'
|
||||||
values={{typeName: badge.type_name}}
|
values={{typeName: badge.type_name}}
|
||||||
/>
|
/>
|
||||||
|
{' · '}
|
||||||
|
{getGrantedText(badge)}
|
||||||
</div>
|
</div>
|
||||||
<div className='granted-by'>{getGrantedText(badge)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,26 +3,96 @@
|
|||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.badge-info {
|
.badge-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
padding: 5px;
|
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.badge-icon {
|
.badge-icon {
|
||||||
padding: 10px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-right: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-label {
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.badge-name {
|
.badge-name {
|
||||||
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: var(--center-channel-text);
|
||||||
}
|
}
|
||||||
.created-by {
|
|
||||||
font-size: 10px;
|
.badge-description {
|
||||||
}
|
font-size: 14px;
|
||||||
.badge-descrition {
|
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0px
|
margin: 0;
|
||||||
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.badge-type {
|
|
||||||
font-size: 10px;
|
.badge-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__editButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.16);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--button-bg, #166de0);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(var(--button-bg-rgb, 22, 109, 224), 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--center-channel-text);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-owners {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||||
|
padding: 16px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ type Props = {
|
|||||||
setRHSView: (view: RHSState) => void;
|
setRHSView: (view: RHSState) => void;
|
||||||
setRHSUser: (user: string | null) => void;
|
setRHSUser: (user: string | null) => void;
|
||||||
getCustomEmojiByName: (names: string) => void;
|
getCustomEmojiByName: (names: string) => void;
|
||||||
|
openEditBadgeModal: (badge: BadgeDetails) => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,11 +99,8 @@ class BadgeDetailsComponent extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (<div>
|
return (<div className='BadgeDetails BadgeDetails--loading'>
|
||||||
<FormattedMessage
|
<div className='spinner'/>
|
||||||
id='badges.loading'
|
|
||||||
defaultMessage='Загрузка...'
|
|
||||||
/>
|
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,30 +124,41 @@ class BadgeDetailsComponent extends React.PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div className='BadgeDetails'>
|
<div className='BadgeDetails'>
|
||||||
<div><b>
|
|
||||||
<FormattedMessage
|
|
||||||
id='badges.rhs.badge_details'
|
|
||||||
defaultMessage='Детали значка'
|
|
||||||
/>
|
|
||||||
</b></div>
|
|
||||||
<div className='badge-info'>
|
<div className='badge-info'>
|
||||||
<span className='badge-icon'>
|
<span className='badge-icon'>
|
||||||
<BadgeImage
|
<BadgeImage
|
||||||
badge={badge}
|
badge={badge}
|
||||||
size={32}
|
size={48}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div className='badge-text'>
|
<div className='badge-text'>
|
||||||
<div className='badge-name'>{badge.name}</div>
|
<div className='badge-name'>
|
||||||
<div className='badge-description'>{markdown(badge.description)}</div>
|
<span className='badge-label'>
|
||||||
<div className='badge-type'>
|
<FormattedMessage
|
||||||
|
id='badges.label.name'
|
||||||
|
defaultMessage='Название:'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{' '}
|
||||||
|
{badge.name}
|
||||||
|
</div>
|
||||||
|
<div className='badge-description'>
|
||||||
|
<span className='badge-label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.label.description'
|
||||||
|
defaultMessage='Описание:'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{' '}
|
||||||
|
{markdown(badge.description)}
|
||||||
|
</div>
|
||||||
|
<div className='badge-meta'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.label.type'
|
id='badges.label.type'
|
||||||
defaultMessage='Тип: {typeName}'
|
defaultMessage='Тип: {typeName}'
|
||||||
values={{typeName: badge.type_name}}
|
values={{typeName: badge.type_name}}
|
||||||
/>
|
/>
|
||||||
</div>
|
{' · '}
|
||||||
<div className='created-by'>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.label.created_by'
|
id='badges.label.created_by'
|
||||||
defaultMessage='Создал: {username}'
|
defaultMessage='Создал: {username}'
|
||||||
@ -157,14 +166,36 @@ class BadgeDetailsComponent extends React.PureComponent<Props, State> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{badge.created_by === this.props.currentUserID && (
|
||||||
|
<button
|
||||||
|
className='BadgeDetails__editButton'
|
||||||
|
onClick={() => this.props.actions.openEditBadgeModal(badge)}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.rhs.edit_badge'
|
||||||
|
defaultMessage='Редактировать'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div><b>
|
{badge.owners.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className='section-title'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.granted_to'
|
id='badges.granted_to'
|
||||||
defaultMessage='Выдан:'
|
defaultMessage='Выдан:'
|
||||||
/>
|
/>
|
||||||
</b></div>
|
</div>
|
||||||
<RHSScrollbars>{content}</RHSScrollbars>
|
<RHSScrollbars>{content}</RHSScrollbars>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className='empty-owners'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.not_granted_yet'
|
||||||
|
defaultMessage='Ещё никому не выдан'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import {getCustomEmojiByName, getCustomEmojisByName} from 'mattermost-redux/acti
|
|||||||
import {getRHSBadge, getRHSUser, getRHSView} from 'selectors';
|
import {getRHSBadge, getRHSUser, getRHSView} from 'selectors';
|
||||||
import {RHS_STATE_ALL, RHS_STATE_DETAIL, RHS_STATE_OTHER, RHS_STATE_MY} from '../../constants';
|
import {RHS_STATE_ALL, RHS_STATE_DETAIL, RHS_STATE_OTHER, RHS_STATE_MY} from '../../constants';
|
||||||
import {RHSState} from 'types/general';
|
import {RHSState} from 'types/general';
|
||||||
import {setRHSBadge, setRHSUser, setRHSView} from 'actions/actions';
|
import {openCreateBadgeModal, openEditBadgeModal, setRHSBadge, setRHSUser, setRHSView} from 'actions/actions';
|
||||||
import {BadgeID} from 'types/badges';
|
import {BadgeDetails, BadgeID} from 'types/badges';
|
||||||
|
|
||||||
import UserBadges from './user_badges';
|
import UserBadges from './user_badges';
|
||||||
import BadgeDetailsComponent from './badge_details';
|
import BadgeDetailsComponent from './badge_details';
|
||||||
@ -39,6 +39,7 @@ const RHS: React.FC = () => {
|
|||||||
setRHSView: (view: RHSState) => dispatch(setRHSView(view)),
|
setRHSView: (view: RHSState) => dispatch(setRHSView(view)),
|
||||||
setRHSBadge: (badge: BadgeID | null) => dispatch(setRHSBadge(badge)),
|
setRHSBadge: (badge: BadgeID | null) => dispatch(setRHSBadge(badge)),
|
||||||
getCustomEmojisByName: (names: string[]) => dispatch(getCustomEmojisByName(names)),
|
getCustomEmojisByName: (names: string[]) => dispatch(getCustomEmojisByName(names)),
|
||||||
|
openCreateBadgeModal: () => dispatch(openCreateBadgeModal()),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -51,6 +52,7 @@ const RHS: React.FC = () => {
|
|||||||
setRHSView: (view: RHSState) => dispatch(setRHSView(view)),
|
setRHSView: (view: RHSState) => dispatch(setRHSView(view)),
|
||||||
setRHSUser: (user: string | null) => dispatch(setRHSUser(user)),
|
setRHSUser: (user: string | null) => dispatch(setRHSUser(user)),
|
||||||
getCustomEmojiByName: (names: string) => dispatch(getCustomEmojiByName(names)),
|
getCustomEmojiByName: (names: string) => dispatch(getCustomEmojiByName(names)),
|
||||||
|
openEditBadgeModal: (badge: BadgeDetails) => dispatch(openEditBadgeModal(badge)),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,28 +1,76 @@
|
|||||||
.UserBadgesRow {
|
.UserBadgesRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
|
||||||
border-radius: 4px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px;
|
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||||
margin-bottom: 3px;
|
border-radius: 6px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 12px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(var(--center-channel-color-rgb), 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
.user-badge-icon {
|
.user-badge-icon {
|
||||||
padding: 10px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-badge-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.user-badge-name {
|
.user-badge-name {
|
||||||
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: var(--center-channel-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.user-badge-granted-by {
|
|
||||||
font-size: 10px;
|
.user-badge-description {
|
||||||
}
|
font-size: 13px;
|
||||||
.user-badge-granted-at {
|
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||||
font-size: 10px;
|
margin-top: 2px;
|
||||||
}
|
|
||||||
.user-badge-descrition {
|
|
||||||
p {
|
p {
|
||||||
margin: 0px
|
margin: 0;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badge-label {
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badge-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badge-reason {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badge-set-status {
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--button-bg, #166de0);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.user-badge-type {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
|
||||||
import Client4 from 'mattermost-redux/client/client4';
|
import Client4 from 'mattermost-redux/client/client4';
|
||||||
|
|
||||||
@ -17,11 +17,12 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) => {
|
const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) => {
|
||||||
|
const intl = useIntl();
|
||||||
const time = new Date(badge.time);
|
const time = new Date(badge.time);
|
||||||
let reason = null;
|
let reason = null;
|
||||||
if (badge.reason) {
|
if (badge.reason) {
|
||||||
reason = (
|
reason = (
|
||||||
<div className='badge-user-reason'>
|
<div className='user-badge-reason'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.label.reason'
|
id='badges.label.reason'
|
||||||
defaultMessage='Причина: {reason}'
|
defaultMessage='Причина: {reason}'
|
||||||
@ -35,7 +36,8 @@ const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) =
|
|||||||
setStatus = (
|
setStatus = (
|
||||||
<div className='user-badge-set-status'>
|
<div className='user-badge-set-status'>
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
const c = new Client4();
|
const c = new Client4();
|
||||||
c.updateCustomStatus({emoji: badge.image, text: badge.name});
|
c.updateCustomStatus({emoji: badge.image, text: badge.name});
|
||||||
}}
|
}}
|
||||||
@ -49,40 +51,59 @@ const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) =
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className='UserBadgesRow'>
|
<div
|
||||||
<a onClick={() => onClick(badge)}>
|
className='UserBadgesRow'
|
||||||
|
onClick={() => onClick(badge)}
|
||||||
|
>
|
||||||
<span className='user-badge-icon'>
|
<span className='user-badge-icon'>
|
||||||
<BadgeImage
|
<BadgeImage
|
||||||
badge={badge}
|
badge={badge}
|
||||||
size={32}
|
size={36}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
|
||||||
<div className='user-badge-text'>
|
<div className='user-badge-text'>
|
||||||
<div className='user-badge-name'>{badge.name}</div>
|
<div className='user-badge-name'>
|
||||||
<div className='user-badge-description'>{markdown(badge.description)}</div>
|
<span className='user-badge-label'>
|
||||||
{reason}
|
<FormattedMessage
|
||||||
<div className='user-badge-type'>
|
id='badges.label.name'
|
||||||
|
defaultMessage='Название:'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{' '}
|
||||||
|
{badge.name}
|
||||||
|
</div>
|
||||||
|
<div className='user-badge-description'>
|
||||||
|
<span className='user-badge-label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='badges.label.description'
|
||||||
|
defaultMessage='Описание:'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{' '}
|
||||||
|
{markdown(badge.description)}
|
||||||
|
</div>
|
||||||
|
<div className='user-badge-meta'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.label.type'
|
id='badges.label.type'
|
||||||
defaultMessage='Тип: {typeName}'
|
defaultMessage='Тип: {typeName}'
|
||||||
values={{typeName: badge.type_name}}
|
values={{typeName: badge.type_name}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='user-badge-granted-by'>
|
<div className='user-badge-meta'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.label.granted_by'
|
id='badges.label.granted_by'
|
||||||
defaultMessage='Выдал: {username}'
|
defaultMessage='Выдал: {username}'
|
||||||
values={{username: badge.granted_by_name}}
|
values={{username: badge.granted_by_name}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='user-badge-granted-at'>
|
<div className='user-badge-meta'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.label.granted_at'
|
id='badges.label.granted_at'
|
||||||
defaultMessage='Выдан: {date}'
|
defaultMessage='Выдан: {date}'
|
||||||
values={{date: time.toDateString()}}
|
values={{date: intl.formatDate(time, {day: '2-digit', month: '2-digit', year: 'numeric'})}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{reason}
|
||||||
{setStatus}
|
{setStatus}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,4 +3,20 @@
|
|||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,11 +95,8 @@ class UserBadges extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
return (<div>
|
return (<div className='UserBadges UserBadges--loading'>
|
||||||
<FormattedMessage
|
<div className='spinner'/>
|
||||||
id='badges.loading'
|
|
||||||
defaultMessage='Загрузка...'
|
|
||||||
/>
|
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +134,7 @@ class UserBadges extends React.PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className='UserBadges'>
|
<div className='UserBadges'>
|
||||||
<div><b>{title}</b></div>
|
<div className='UserBadges__title'>{title}</div>
|
||||||
<RHSScrollbars>{content}</RHSScrollbars>
|
<RHSScrollbars>{content}</RHSScrollbars>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,13 +1,32 @@
|
|||||||
.UserRow {
|
.UserRow {
|
||||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
align-items: center;
|
padding: 10px 16px;
|
||||||
padding: 5px;
|
margin-bottom: 8px;
|
||||||
margin-bottom: 3px;
|
cursor: pointer;
|
||||||
.badge-user-username {
|
transition: background 0.15s;
|
||||||
font-weight: 600;
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(var(--center-channel-color-rgb), 0.04);
|
||||||
}
|
}
|
||||||
.badge-user-granted-at {
|
|
||||||
font-size: 10px;
|
.badge-user-username {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--button-bg, #166de0);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-user-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||||
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {UserProfile} from 'mattermost-redux/types/users';
|
|||||||
import {Ownership} from '../../types/badges';
|
import {Ownership} from '../../types/badges';
|
||||||
|
|
||||||
import './user_row.scss';
|
import './user_row.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
ownership: Ownership;
|
ownership: Ownership;
|
||||||
onClick: (user: string) => void;
|
onClick: (user: string) => void;
|
||||||
@ -31,20 +32,24 @@ const UserBadgeRow: React.FC<Props> = ({ownership, onClick}: Props) => {
|
|||||||
|
|
||||||
const time = new Date(ownership.time);
|
const time = new Date(ownership.time);
|
||||||
return (
|
return (
|
||||||
<div className='UserRow'>
|
<div
|
||||||
<div className='badge-user-username'><a onClick={() => onClick(ownership.user)}>{`@${user.username}`}</a></div>
|
className='UserRow'
|
||||||
<div className='badge-user-granted-by'>
|
onClick={() => onClick(ownership.user)}
|
||||||
|
>
|
||||||
|
<div className='badge-user-username'>
|
||||||
|
<a>{`@${user.username}`}</a>
|
||||||
|
</div>
|
||||||
|
<div className='badge-user-meta'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.label.granted_by'
|
id='badges.label.granted_by'
|
||||||
defaultMessage='Выдал: {username}'
|
defaultMessage='Выдал: {username}'
|
||||||
values={{username: grantedByName}}
|
values={{username: grantedByName}}
|
||||||
/>
|
/>
|
||||||
</div>
|
{' · '}
|
||||||
<div className='badge-user-granted-at'>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.label.granted_at'
|
id='badges.label.granted_at'
|
||||||
defaultMessage='Выдан: {date}'
|
defaultMessage='Выдан: {date}'
|
||||||
values={{date: time.toDateString()}}
|
values={{date: intl.formatDate(time, {day: '2-digit', month: '2-digit', year: 'numeric'})}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -112,6 +112,12 @@ class BadgeList extends React.PureComponent<Props, State> {
|
|||||||
for (let i = 0; i < toShow; i++) {
|
for (let i = 0; i < toShow; i++) {
|
||||||
const badge = this.state.badges![i];
|
const badge = this.state.badges![i];
|
||||||
const time = new Date(badge.time);
|
const time = new Date(badge.time);
|
||||||
|
const nameLabel = intl.formatMessage(
|
||||||
|
{id: 'badges.label.name', defaultMessage: 'Название:'},
|
||||||
|
);
|
||||||
|
const descLabel = intl.formatMessage(
|
||||||
|
{id: 'badges.label.description', defaultMessage: 'Описание:'},
|
||||||
|
);
|
||||||
let reason: string | null = null;
|
let reason: string | null = null;
|
||||||
if (badge.reason) {
|
if (badge.reason) {
|
||||||
reason = intl.formatMessage(
|
reason = intl.formatMessage(
|
||||||
@ -125,11 +131,11 @@ class BadgeList extends React.PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
const grantedAt = intl.formatMessage(
|
const grantedAt = intl.formatMessage(
|
||||||
{id: 'badges.label.granted_at', defaultMessage: 'Выдан: {date}'},
|
{id: 'badges.label.granted_at', defaultMessage: 'Выдан: {date}'},
|
||||||
{date: time.toDateString()},
|
{date: intl.formatDate(time, {day: '2-digit', month: '2-digit', year: 'numeric'})},
|
||||||
);
|
);
|
||||||
const tooltipLines = [
|
const tooltipLines = [
|
||||||
badge.name,
|
nameLabel + ' ' + badge.name,
|
||||||
badge.description,
|
descLabel + ' ' + badge.description,
|
||||||
reason,
|
reason,
|
||||||
grantedBy,
|
grantedBy,
|
||||||
grantedAt,
|
grantedAt,
|
||||||
@ -170,10 +176,7 @@ class BadgeList extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
// Reserve enough height one row of badges and the "and more" button
|
// Reserve enough height one row of badges and the "and more" button
|
||||||
<div style={{height: BADGE_SIZE, minWidth: 66, maxWidth}}>
|
<div style={{height: BADGE_SIZE, minWidth: 66, maxWidth}}>
|
||||||
<FormattedMessage
|
<div className='spinner'/>
|
||||||
id='badges.loading'
|
|
||||||
defaultMessage='Загрузка...'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,4 +14,6 @@ export const initialState: PluginState = {
|
|||||||
rhsView: RHS_STATE_MY,
|
rhsView: RHS_STATE_MY,
|
||||||
rhsBadge: null,
|
rhsBadge: null,
|
||||||
rhsUser: null,
|
rhsUser: null,
|
||||||
|
createBadgeModalVisible: false,
|
||||||
|
editBadgeModalData: null,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {useSelector} from 'react-redux';
|
|||||||
import {openAddSubscription, openCreateBadge, openCreateType, openRemoveSubscription, setRHSView, setShowRHSAction} from 'actions/actions';
|
import {openAddSubscription, openCreateBadge, openCreateType, openRemoveSubscription, setRHSView, setShowRHSAction} from 'actions/actions';
|
||||||
|
|
||||||
import RHSComponent from 'components/rhs';
|
import RHSComponent from 'components/rhs';
|
||||||
|
import BadgeModal from 'components/badge_modal';
|
||||||
|
|
||||||
import ChannelHeaderButton from 'components/channel_header_button';
|
import ChannelHeaderButton from 'components/channel_header_button';
|
||||||
|
|
||||||
@ -60,6 +61,8 @@ export default class Plugin {
|
|||||||
|
|
||||||
registry.registerPopoverUserAttributesComponent(WrappedBadgeList);
|
registry.registerPopoverUserAttributesComponent(WrappedBadgeList);
|
||||||
|
|
||||||
|
registry.registerRootComponent(withIntl(BadgeModal));
|
||||||
|
|
||||||
const locale = getCurrentUser(store.getState())?.locale || 'ru';
|
const locale = getCurrentUser(store.getState())?.locale || 'ru';
|
||||||
const messages = getTranslations(locale);
|
const messages = getTranslations(locale);
|
||||||
|
|
||||||
|
|||||||
@ -41,9 +41,33 @@ function rhsBadge(state = null, action: GenericAction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createBadgeModalVisible(state = false, action: GenericAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.OPEN_CREATE_BADGE_MODAL:
|
||||||
|
return true;
|
||||||
|
case ActionTypes.CLOSE_CREATE_BADGE_MODAL:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editBadgeModalData(state = null, action: GenericAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.OPEN_EDIT_BADGE_MODAL:
|
||||||
|
return action.data;
|
||||||
|
case ActionTypes.CLOSE_EDIT_BADGE_MODAL:
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
showRHS,
|
showRHS,
|
||||||
rhsView,
|
rhsView,
|
||||||
rhsUser,
|
rhsUser,
|
||||||
rhsBadge,
|
rhsBadge,
|
||||||
|
createBadgeModalVisible,
|
||||||
|
editBadgeModalData,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -43,3 +43,17 @@ export const getRHSBadge = createSelector(
|
|||||||
return state.rhsBadge;
|
return state.rhsBadge;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const isCreateBadgeModalVisible = createSelector(
|
||||||
|
getPluginState,
|
||||||
|
(state) => {
|
||||||
|
return state.createBadgeModalVisible;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getEditBadgeModalData = createSelector(
|
||||||
|
getPluginState,
|
||||||
|
(state) => {
|
||||||
|
return state.editBadgeModalData;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -42,4 +42,46 @@ export type BadgeTypeDefinition = {
|
|||||||
id: BadgeType;
|
id: BadgeType;
|
||||||
name: string;
|
name: string;
|
||||||
frame: string;
|
frame: string;
|
||||||
|
created_by: string;
|
||||||
|
can_grant: PermissionScheme;
|
||||||
|
can_create: PermissionScheme;
|
||||||
|
badge_count: number;
|
||||||
|
is_default: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PermissionScheme = {
|
||||||
|
everyone: boolean;
|
||||||
|
roles: Record<string, boolean>;
|
||||||
|
allow_list: Record<string, boolean>;
|
||||||
|
block_list: Record<string, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetTypesResponse = {
|
||||||
|
types: BadgeTypeDefinition[];
|
||||||
|
can_create_type: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateBadgeRequest = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
type: string;
|
||||||
|
multiple: boolean;
|
||||||
|
channel_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateBadgeRequest = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
type: string;
|
||||||
|
multiple: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateTypeRequest = {
|
||||||
|
name: string;
|
||||||
|
everyone_can_create: boolean;
|
||||||
|
everyone_can_grant: boolean;
|
||||||
|
channel_id?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {BadgeID} from './badges';
|
import {BadgeDetails, BadgeID} from './badges';
|
||||||
|
|
||||||
export type RHSState = string;
|
export type RHSState = string;
|
||||||
|
|
||||||
@ -7,4 +7,6 @@ export type PluginState = {
|
|||||||
rhsView: RHSState;
|
rhsView: RHSState;
|
||||||
rhsUser: string | null;
|
rhsUser: string | null;
|
||||||
rhsBadge: BadgeID | null;
|
rhsBadge: BadgeID | null;
|
||||||
|
createBadgeModalVisible: boolean;
|
||||||
|
editBadgeModalData: BadgeDetails | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export interface PluginRegistry {
|
|||||||
registerAppBarComponent(iconURL: string, action: (channel: Channel, member: ChannelMembership) => void, tooltipText: React.ReactNode);
|
registerAppBarComponent(iconURL: string, action: (channel: Channel, member: ChannelMembership) => void, tooltipText: React.ReactNode);
|
||||||
registerTranslations(getTranslationsForLocale: (locale: string) => Record<string, string>): void;
|
registerTranslations(getTranslationsForLocale: (locale: string) => Record<string, string>): void;
|
||||||
registerAdminConsoleCustomSetting(key: string, component: React.ElementType, options?: {showTitle: boolean}): void;
|
registerAdminConsoleCustomSetting(key: string, component: React.ElementType, options?: {showTitle: boolean}): void;
|
||||||
|
registerRootComponent(component: React.ElementType): void;
|
||||||
|
|
||||||
// Add more if needed from https://developers.mattermost.com/extend/plugins/webapp/reference
|
// Add more if needed from https://developers.mattermost.com/extend/plugins/webapp/reference
|
||||||
}
|
}
|
||||||
|
|||||||
9
webapp/src/utils/helpers.ts
Normal file
9
webapp/src/utils/helpers.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export function getServerErrorId(err: unknown): string {
|
||||||
|
const msg = (err as {message?: string})?.message || '';
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(msg);
|
||||||
|
return parsed.id || '';
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1768,33 +1768,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@giphy/js-fetch-api@npm:^5.1.0":
|
|
||||||
version: 5.7.0
|
|
||||||
resolution: "@giphy/js-fetch-api@npm:5.7.0"
|
|
||||||
dependencies:
|
|
||||||
"@giphy/js-types": "npm:*"
|
|
||||||
"@giphy/js-util": "npm:*"
|
|
||||||
checksum: 10c0/af1990c49ed4d633be04e497f6575e4f798c61348cfca9907f74a8450746bb6ab7336b53eea99e647b902076016d994264979bd09a9aacaa85d40cd610a525ac
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@giphy/js-types@npm:*":
|
|
||||||
version: 5.1.0
|
|
||||||
resolution: "@giphy/js-types@npm:5.1.0"
|
|
||||||
checksum: 10c0/8a76b9fd72d10d47486f26902a2fdc083712b4e0582bb2b27698b2c9c58fd3ecfa07d14ac30bcb80ec0f2ec35b3de7f7791d9d9751b0e6cb7d6fa5c39d52479f
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@giphy/js-util@npm:*":
|
|
||||||
version: 5.2.0
|
|
||||||
resolution: "@giphy/js-util@npm:5.2.0"
|
|
||||||
dependencies:
|
|
||||||
"@giphy/js-types": "npm:*"
|
|
||||||
uuid: "npm:^9.0.0"
|
|
||||||
checksum: 10c0/0782a4fa1d7b037b4010f76966f5b8347c5732f57516d191471d746cd733f2d212f7461c4ff613961e51cbd65584a1f5e7202dbe5ae2231978b5c52f51a2e7f7
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@isaacs/balanced-match@npm:^4.0.1":
|
"@isaacs/balanced-match@npm:^4.0.1":
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
resolution: "@isaacs/balanced-match@npm:4.0.1"
|
resolution: "@isaacs/balanced-match@npm:4.0.1"
|
||||||
@ -5921,19 +5894,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"form-data@npm:^4.0.0":
|
|
||||||
version: 4.0.5
|
|
||||||
resolution: "form-data@npm:4.0.5"
|
|
||||||
dependencies:
|
|
||||||
asynckit: "npm:^0.4.0"
|
|
||||||
combined-stream: "npm:^1.0.8"
|
|
||||||
es-set-tostringtag: "npm:^2.1.0"
|
|
||||||
hasown: "npm:^2.0.2"
|
|
||||||
mime-types: "npm:^2.1.12"
|
|
||||||
checksum: 10c0/dd6b767ee0bbd6d84039db12a0fa5a2028160ffbfaba1800695713b46ae974a5f6e08b3356c3195137f8530dcd9dfcb5d5ae1eeff53d0db1e5aad863b619ce3b
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"fragment-cache@npm:^0.2.1":
|
"fragment-cache@npm:^0.2.1":
|
||||||
version: 0.2.1
|
version: 0.2.1
|
||||||
resolution: "fragment-cache@npm:0.2.1"
|
resolution: "fragment-cache@npm:0.2.1"
|
||||||
@ -8062,20 +8022,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"loop-plugin-sdk@https://artifacts.wilix.dev/repository/npm-public-loop/loop-plugin-sdk/-/loop-plugin-sdk-0.1.6.tgz":
|
|
||||||
version: 0.1.6
|
|
||||||
resolution: "loop-plugin-sdk@https://artifacts.wilix.dev/repository/npm-public-loop/loop-plugin-sdk/-/loop-plugin-sdk-0.1.6.tgz"
|
|
||||||
dependencies:
|
|
||||||
"@giphy/js-fetch-api": "npm:^5.1.0"
|
|
||||||
form-data: "npm:^4.0.0"
|
|
||||||
rudder-sdk-js: "npm:^2.41.0"
|
|
||||||
serialize-error: "npm:^11.0.2"
|
|
||||||
shallow-equals: "npm:^1.0.0"
|
|
||||||
timezones.json: "npm:^1.7.1"
|
|
||||||
checksum: 10c0/661ed3b99bb666a5fe024dadbbb2f1cf6d7d43bed3e94e87171e76985b31f82b8b4042358f30b60bd097e452185c957ec55fc03b15f45ac1ca70a6239fac1b71
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
|
"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
resolution: "loose-envify@npm:1.4.0"
|
resolution: "loose-envify@npm:1.4.0"
|
||||||
@ -10223,7 +10169,6 @@ __metadata:
|
|||||||
jest: "npm:26.6.3"
|
jest: "npm:26.6.3"
|
||||||
jest-canvas-mock: "npm:2.3.1"
|
jest-canvas-mock: "npm:2.3.1"
|
||||||
jest-junit: "npm:12.0.0"
|
jest-junit: "npm:12.0.0"
|
||||||
loop-plugin-sdk: "https://artifacts.wilix.dev/repository/npm-public-loop/loop-plugin-sdk/-/loop-plugin-sdk-0.1.6.tgz"
|
|
||||||
mattermost-redux: "npm:5.33.1"
|
mattermost-redux: "npm:5.33.1"
|
||||||
memoize-one: "npm:^5.2.1"
|
memoize-one: "npm:^5.2.1"
|
||||||
react: "npm:17.0.2"
|
react: "npm:17.0.2"
|
||||||
@ -10264,13 +10209,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"rudder-sdk-js@npm:^2.41.0":
|
|
||||||
version: 2.52.8
|
|
||||||
resolution: "rudder-sdk-js@npm:2.52.8"
|
|
||||||
checksum: 10c0/62732c3402bf1858c1100b287bd72d7e54c293b308f2e96c633aa29dfd8758bbee50b1aa93011ea98d4960b8fb98d2ab88a69536b5fe35bd794e85d5cd4ae861
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"run-parallel@npm:^1.1.9":
|
"run-parallel@npm:^1.1.9":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "run-parallel@npm:1.2.0"
|
resolution: "run-parallel@npm:1.2.0"
|
||||||
@ -10514,15 +10452,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"serialize-error@npm:^11.0.2":
|
|
||||||
version: 11.0.3
|
|
||||||
resolution: "serialize-error@npm:11.0.3"
|
|
||||||
dependencies:
|
|
||||||
type-fest: "npm:^2.12.2"
|
|
||||||
checksum: 10c0/7263603883b8936650819f0fd5150d41427b317432678b21722c54b85367ae15b8552865eb7f3f39ba71a32a003730a2e2e971e6909431eb54db70a3ef8eca17
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"serialize-javascript@npm:^6.0.2":
|
"serialize-javascript@npm:^6.0.2":
|
||||||
version: 6.0.2
|
version: 6.0.2
|
||||||
resolution: "serialize-javascript@npm:6.0.2"
|
resolution: "serialize-javascript@npm:6.0.2"
|
||||||
@ -10617,7 +10546,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"shallow-equals@npm:1.0.0, shallow-equals@npm:^1.0.0":
|
"shallow-equals@npm:1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "shallow-equals@npm:1.0.0"
|
resolution: "shallow-equals@npm:1.0.0"
|
||||||
checksum: 10c0/ba7c87947126fcfdd31d6c473c5785c235c385dcc045dd6b1543366b1e86aa8e8f69289346ffee0b13a845365fc6f3d21badd8c00e2b6c2b1d0e84d69bcf4487
|
checksum: 10c0/ba7c87947126fcfdd31d6c473c5785c235c385dcc045dd6b1543366b1e86aa8e8f69289346ffee0b13a845365fc6f3d21badd8c00e2b6c2b1d0e84d69bcf4487
|
||||||
@ -11363,13 +11292,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"timezones.json@npm:^1.7.1":
|
|
||||||
version: 1.7.2
|
|
||||||
resolution: "timezones.json@npm:1.7.2"
|
|
||||||
checksum: 10c0/209da3d2334118790f57ad060de68ba799adde9df051a6428c348079ed0df20a753e190f85cceadab336f6b24a68302bcca84c895c70270126b15ea0bcde77cd
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"tinyglobby@npm:^0.2.12":
|
"tinyglobby@npm:^0.2.12":
|
||||||
version: 0.2.15
|
version: 0.2.15
|
||||||
resolution: "tinyglobby@npm:0.2.15"
|
resolution: "tinyglobby@npm:0.2.15"
|
||||||
@ -11586,13 +11508,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"type-fest@npm:^2.12.2":
|
|
||||||
version: 2.19.0
|
|
||||||
resolution: "type-fest@npm:2.19.0"
|
|
||||||
checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"typed-array-buffer@npm:^1.0.3":
|
"typed-array-buffer@npm:^1.0.3":
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
resolution: "typed-array-buffer@npm:1.0.3"
|
resolution: "typed-array-buffer@npm:1.0.3"
|
||||||
@ -11895,15 +11810,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"uuid@npm:^9.0.0":
|
|
||||||
version: 9.0.1
|
|
||||||
resolution: "uuid@npm:9.0.1"
|
|
||||||
bin:
|
|
||||||
uuid: dist/bin/uuid
|
|
||||||
checksum: 10c0/1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"v8-compile-cache@npm:^2.0.3, v8-compile-cache@npm:^2.2.0":
|
"v8-compile-cache@npm:^2.0.3, v8-compile-cache@npm:^2.2.0":
|
||||||
version: 2.4.0
|
version: 2.4.0
|
||||||
resolution: "v8-compile-cache@npm:2.4.0"
|
resolution: "v8-compile-cache@npm:2.4.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user