LP-5613 #2

Open
dmitrii.pichenikin wants to merge 37 commits from LP-5613 into dev
28 changed files with 582 additions and 347 deletions
Showing only changes of commit df25e1f6fc - Show all commits

View File

@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"runtime/debug" "runtime/debug"
"strings" "strings"
@ -81,6 +82,12 @@ type SubscriptionAPIRequest struct {
ChannelID string `json:"channel_id"` ChannelID string `json:"channel_id"`
} }
type RevokeOwnershipRequest struct {
BadgeID string `json:"badge_id"`
UserID string `json:"user_id"`
Time string `json:"time"`
}
type TypeWithBadgeCount struct { type TypeWithBadgeCount struct {
*badgesmodel.BadgeTypeDefinition *badgesmodel.BadgeTypeDefinition
BadgeCount int `json:"badge_count"` BadgeCount int `json:"badge_count"`
@ -114,6 +121,7 @@ func (p *Plugin) initializeAPI() {
apiRouter.HandleFunc("/deleteBadge/{badgeID}", p.extractUserMiddleWare(p.apiDeleteBadge, ResponseTypeJSON)).Methods(http.MethodDelete) apiRouter.HandleFunc("/deleteBadge/{badgeID}", p.extractUserMiddleWare(p.apiDeleteBadge, ResponseTypeJSON)).Methods(http.MethodDelete)
apiRouter.HandleFunc("/deleteType/{typeID}", p.extractUserMiddleWare(p.apiDeleteType, ResponseTypeJSON)).Methods(http.MethodDelete) apiRouter.HandleFunc("/deleteType/{typeID}", p.extractUserMiddleWare(p.apiDeleteType, ResponseTypeJSON)).Methods(http.MethodDelete)
apiRouter.HandleFunc("/grantBadge", p.extractUserMiddleWare(p.apiGrantBadge, ResponseTypeJSON)).Methods(http.MethodPost) apiRouter.HandleFunc("/grantBadge", p.extractUserMiddleWare(p.apiGrantBadge, ResponseTypeJSON)).Methods(http.MethodPost)
apiRouter.HandleFunc("/revokeOwnership", p.extractUserMiddleWare(p.apiRevokeOwnership, ResponseTypeJSON)).Methods(http.MethodPost)
apiRouter.HandleFunc("/getChannelSubscriptions/{channelID}", p.extractUserMiddleWare(p.apiGetChannelSubscriptions, ResponseTypeJSON)).Methods(http.MethodGet) apiRouter.HandleFunc("/getChannelSubscriptions/{channelID}", p.extractUserMiddleWare(p.apiGetChannelSubscriptions, ResponseTypeJSON)).Methods(http.MethodGet)
apiRouter.HandleFunc("/createSubscription", p.extractUserMiddleWare(p.apiCreateSubscription, ResponseTypeJSON)).Methods(http.MethodPost) apiRouter.HandleFunc("/createSubscription", p.extractUserMiddleWare(p.apiCreateSubscription, ResponseTypeJSON)).Methods(http.MethodPost)
apiRouter.HandleFunc("/deleteSubscription", p.extractUserMiddleWare(p.apiDeleteSubscription, ResponseTypeJSON)).Methods(http.MethodPost) apiRouter.HandleFunc("/deleteSubscription", p.extractUserMiddleWare(p.apiDeleteSubscription, ResponseTypeJSON)).Methods(http.MethodPost)
@ -1123,6 +1131,10 @@ func (p *Plugin) dialogGrant(w http.ResponseWriter, r *http.Request, userID stri
reason, _ := req.Submission[DialogFieldGrantReason].(string) reason, _ := req.Submission[DialogFieldGrantReason].(string)
shouldNotify, err := p.store.GrantBadge(badgesmodel.BadgeID(badgeIDStr), grantToID, userID, reason) shouldNotify, err := p.store.GrantBadge(badgesmodel.BadgeID(badgeIDStr), grantToID, userID, reason)
if err == errAlreadyOwned {
dialogError(w, T("badges.error.already_owned", "Это достижение уже выдано этому пользователю"), nil)
return
}
if err != nil { if err != nil {
p.writeAPIError(w, &APIErrorResponse{ p.writeAPIError(w, &APIErrorResponse{
ID: "cannot grant badge", ID: "cannot grant badge",
@ -1302,6 +1314,12 @@ func (p *Plugin) grantBadge(w http.ResponseWriter, r *http.Request, pluginID str
} }
shouldNotify, err := p.store.GrantBadge(req.BadgeID, req.UserID, req.BotID, req.Reason) shouldNotify, err := p.store.GrantBadge(req.BadgeID, req.UserID, req.BotID, req.Reason)
if err == errAlreadyOwned {
p.writeAPIError(w, &APIErrorResponse{
ID: "already_owned", Message: "This badge is already owned by this user", StatusCode: http.StatusConflict,
})
return
}
if err != nil { if err != nil {
p.writeAPIError(w, &APIErrorResponse{ p.writeAPIError(w, &APIErrorResponse{
ID: "cannot grant badge", ID: "cannot grant badge",
@ -1670,6 +1688,12 @@ func (p *Plugin) apiGrantBadge(w http.ResponseWriter, r *http.Request, userID st
} }
shouldNotify, err := p.store.GrantBadge(badgesmodel.BadgeID(req.BadgeID), req.UserID, userID, req.Reason) shouldNotify, err := p.store.GrantBadge(badgesmodel.BadgeID(req.BadgeID), req.UserID, userID, req.Reason)
if errors.Is(err, errAlreadyOwned) {
p.writeAPIError(w, &APIErrorResponse{
ID: "already_owned", Message: "This badge is already owned by this user", StatusCode: http.StatusConflict,
})
return
}
if err != nil { if err != nil {
p.writeAPIError(w, &APIErrorResponse{ p.writeAPIError(w, &APIErrorResponse{
ID: "cannot_grant_badge", Message: err.Error(), StatusCode: http.StatusInternalServerError, ID: "cannot_grant_badge", Message: err.Error(), StatusCode: http.StatusInternalServerError,
@ -1811,3 +1835,50 @@ func (p *Plugin) apiGetChannelSubscriptions(w http.ResponseWriter, r *http.Reque
b, _ := json.Marshal(types) b, _ := json.Marshal(types)
_, _ = w.Write(b) _, _ = w.Write(b)
} }
func (p *Plugin) apiRevokeOwnership(w http.ResponseWriter, r *http.Request, userID string) {
var req RevokeOwnershipRequest
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
}
req.BadgeID = strings.TrimSpace(req.BadgeID)
req.UserID = strings.TrimSpace(req.UserID)
req.Time = strings.TrimSpace(req.Time)
if req.BadgeID == "" || req.UserID == "" || req.Time == "" {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_request", Message: "badge_id, user_id and time are required", StatusCode: http.StatusBadRequest,
})
return
}
ownership, err := p.store.FindOwnership(badgesmodel.BadgeID(req.BadgeID), req.UserID, req.Time)
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "ownership_not_found", Message: "Ownership not found", StatusCode: http.StatusNotFound,
})
return
}
isAdmin := p.badgeAdminUserIDs[userID]
if ownership.GrantedBy != userID && !isAdmin {
p.writeAPIError(w, &APIErrorResponse{
ID: "no_permission_revoke", Message: "No permission to revoke this ownership", StatusCode: http.StatusForbidden,
})
return
}
if err := p.store.RevokeOwnership(badgesmodel.BadgeID(req.BadgeID), req.UserID, req.Time); err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "cannot_revoke", Message: err.Error(), StatusCode: http.StatusInternalServerError,
})
return
}
resp := map[string]string{"status": "ok"}
b, _ := json.Marshal(resp)
_, _ = w.Write(b)
}

View File

@ -592,6 +592,9 @@ func (p *Plugin) runGrant(args []string, extra *model.CommandArgs) (bool, *model
} }
shouldNotify, err := p.store.GrantBadge(badgesmodel.BadgeID(badgeStr), user.Id, extra.UserId, "") shouldNotify, err := p.store.GrantBadge(badgesmodel.BadgeID(badgeStr), user.Id, extra.UserId, "")
if err == errAlreadyOwned {
return commandError(T("badges.error.already_owned", "Это достижение уже выдано этому пользователю"))
}
if err != nil { if err != nil {
return commandError(err.Error()) return commandError(err.Error())
} }

View File

@ -1,21 +1,21 @@
[ [
{"id": "badges.dialog.create_badge.title", "translation": "Create badge"}, {"id": "badges.dialog.create_badge.title", "translation": "Create achievement"},
{"id": "badges.dialog.create_badge.submit", "translation": "Create"}, {"id": "badges.dialog.create_badge.submit", "translation": "Create"},
{"id": "badges.dialog.edit_badge.title", "translation": "Edit badge"}, {"id": "badges.dialog.edit_badge.title", "translation": "Edit achievement"},
{"id": "badges.dialog.edit_badge.submit", "translation": "Save"}, {"id": "badges.dialog.edit_badge.submit", "translation": "Save"},
{"id": "badges.dialog.create_type.title", "translation": "Create type"}, {"id": "badges.dialog.create_type.title", "translation": "Create type"},
{"id": "badges.dialog.create_type.submit", "translation": "Create"}, {"id": "badges.dialog.create_type.submit", "translation": "Create"},
{"id": "badges.dialog.edit_type.title", "translation": "Edit type"}, {"id": "badges.dialog.edit_type.title", "translation": "Edit type"},
{"id": "badges.dialog.edit_type.submit", "translation": "Save"}, {"id": "badges.dialog.edit_type.submit", "translation": "Save"},
{"id": "badges.dialog.grant.title", "translation": "Grant badge"}, {"id": "badges.dialog.grant.title", "translation": "Grant achievement"},
{"id": "badges.dialog.grant.submit", "translation": "Grant"}, {"id": "badges.dialog.grant.submit", "translation": "Grant"},
{"id": "badges.dialog.grant.intro", "translation": "Grant badge to @%s"}, {"id": "badges.dialog.grant.intro", "translation": "Grant achievement to @%s"},
{"id": "badges.dialog.create_subscription.title", "translation": "Create subscription"}, {"id": "badges.dialog.create_subscription.title", "translation": "Create subscription"},
{"id": "badges.dialog.create_subscription.submit", "translation": "Add"}, {"id": "badges.dialog.create_subscription.submit", "translation": "Add"},
{"id": "badges.dialog.create_subscription.intro", "translation": "Select the badge type you want to subscribe to this channel."}, {"id": "badges.dialog.create_subscription.intro", "translation": "Select the achievement type you want to subscribe to this channel."},
{"id": "badges.dialog.delete_subscription.title", "translation": "Delete subscription"}, {"id": "badges.dialog.delete_subscription.title", "translation": "Delete subscription"},
{"id": "badges.dialog.delete_subscription.submit", "translation": "Remove"}, {"id": "badges.dialog.delete_subscription.submit", "translation": "Remove"},
{"id": "badges.dialog.delete_subscription.intro", "translation": "Select the badge type you want to unsubscribe from this channel."}, {"id": "badges.dialog.delete_subscription.intro", "translation": "Select the achievement type you want to unsubscribe from this channel."},
{"id": "badges.field.name", "translation": "Name"}, {"id": "badges.field.name", "translation": "Name"},
{"id": "badges.field.description", "translation": "Description"}, {"id": "badges.field.description", "translation": "Description"},
@ -23,45 +23,46 @@
{"id": "badges.field.image.help", "translation": "Enter an emoticon name"}, {"id": "badges.field.image.help", "translation": "Enter an emoticon name"},
{"id": "badges.field.type", "translation": "Type"}, {"id": "badges.field.type", "translation": "Type"},
{"id": "badges.field.multiple", "translation": "Multiple"}, {"id": "badges.field.multiple", "translation": "Multiple"},
{"id": "badges.field.multiple.help", "translation": "Whether the badge can be granted multiple times"}, {"id": "badges.field.multiple.help", "translation": "Whether the achievement can be granted multiple times"},
{"id": "badges.field.delete_badge", "translation": "Delete badge"}, {"id": "badges.field.delete_badge", "translation": "Delete achievement"},
{"id": "badges.field.delete_badge.help", "translation": "WARNING: checking this will remove this badge permanently."}, {"id": "badges.field.delete_badge.help", "translation": "WARNING: checking this will remove this achievement permanently."},
{"id": "badges.field.everyone_can_create", "translation": "Everyone can create badge"}, {"id": "badges.field.everyone_can_create", "translation": "Everyone can create achievement"},
{"id": "badges.field.everyone_can_create.help", "translation": "Whether any user can create a badge of this type"}, {"id": "badges.field.everyone_can_create.help", "translation": "Whether any user can create an achievement of this type"},
{"id": "badges.field.allowlist_create", "translation": "Can create allowlist"}, {"id": "badges.field.allowlist_create", "translation": "Can create allowlist"},
{"id": "badges.field.allowlist_create.help", "translation": "Fill the usernames separated by comma (,) of the people that can create badges of this type."}, {"id": "badges.field.allowlist_create.help", "translation": "Fill the usernames separated by comma (,) of the people that can create achievements of this type."},
{"id": "badges.field.everyone_can_grant", "translation": "Everyone can grant badge"}, {"id": "badges.field.everyone_can_grant", "translation": "Everyone can grant achievement"},
{"id": "badges.field.everyone_can_grant.help", "translation": "Whether any user can grant a badge of this type"}, {"id": "badges.field.everyone_can_grant.help", "translation": "Whether any user can grant an achievement of this type"},
{"id": "badges.field.allowlist_grant", "translation": "Can grant allowlist"}, {"id": "badges.field.allowlist_grant", "translation": "Can grant allowlist"},
{"id": "badges.field.allowlist_grant.help", "translation": "Fill the usernames separated by comma (,) of the people that can grant badges of this type."}, {"id": "badges.field.allowlist_grant.help", "translation": "Fill the usernames separated by comma (,) of the people that can grant achievements of this type."},
{"id": "badges.field.delete_type", "translation": "Remove type"}, {"id": "badges.field.delete_type", "translation": "Remove type"},
{"id": "badges.field.delete_type.help", "translation": "WARNING: checking this will remove this type and all associated badges permanently."}, {"id": "badges.field.delete_type.help", "translation": "WARNING: checking this will remove this type and all associated achievements permanently."},
{"id": "badges.field.user", "translation": "User"}, {"id": "badges.field.user", "translation": "User"},
{"id": "badges.field.badge", "translation": "Badge"}, {"id": "badges.field.badge", "translation": "Achievement"},
{"id": "badges.field.reason", "translation": "Reason"}, {"id": "badges.field.reason", "translation": "Reason"},
{"id": "badges.field.reason.help", "translation": "Reason why you are granting this badge. This will be seen by the user, and wherever this grant notification is shown (e.g. subscriptions)."}, {"id": "badges.field.reason.help", "translation": "Reason why you are granting this achievement. This will be seen by the user, and wherever this grant notification is shown (e.g. subscriptions)."},
{"id": "badges.field.notify_here", "translation": "Notify on this channel"}, {"id": "badges.field.notify_here", "translation": "Notify on this channel"},
{"id": "badges.field.notify_here.help", "translation": "If you mark this, the bot will send a message to this channel notifying that you granted this badge to this person."}, {"id": "badges.field.notify_here.help", "translation": "If you mark this, the bot will send a message to this channel notifying that you granted this achievement to this person."},
{"id": "badges.error.unknown", "translation": "An unknown error occurred. Please talk to your system administrator for help."}, {"id": "badges.error.unknown", "translation": "An unknown error occurred. Please talk to your system administrator for help."},
{"id": "badges.error.cannot_get_user", "translation": "Cannot get user."}, {"id": "badges.error.cannot_get_user", "translation": "Cannot get user."},
{"id": "badges.error.only_sysadmin_clean", "translation": "Only a system admin can clean the badges database."}, {"id": "badges.error.only_sysadmin_clean", "translation": "Only a system admin can clean the achievements database."},
{"id": "badges.error.specify_create", "translation": "Specify what you want to create."}, {"id": "badges.error.specify_create", "translation": "Specify what you want to create."},
{"id": "badges.error.create_badge_or_type", "translation": "You can create either badge or type"}, {"id": "badges.error.create_badge_or_type", "translation": "You can create either achievement or type"},
{"id": "badges.error.no_types_available", "translation": "You cannot create badges from any type."}, {"id": "badges.error.no_types_available", "translation": "You cannot create achievements from any type."},
{"id": "badges.error.must_set_badge_id", "translation": "You must set the badge ID"}, {"id": "badges.error.must_set_badge_id", "translation": "You must set the achievement ID"},
{"id": "badges.error.cannot_edit_badge", "translation": "You cannot edit this badge"}, {"id": "badges.error.cannot_edit_badge", "translation": "You cannot edit this achievement"},
{"id": "badges.error.specify_edit", "translation": "Specify what you want to edit."}, {"id": "badges.error.specify_edit", "translation": "Specify what you want to edit."},
{"id": "badges.error.edit_badge_or_type", "translation": "You can edit either badge or type"}, {"id": "badges.error.edit_badge_or_type", "translation": "You can edit either achievement or type"},
{"id": "badges.error.no_permissions_edit_type", "translation": "You have no permissions to edit a badge type."}, {"id": "badges.error.no_permissions_edit_type", "translation": "You have no permissions to edit an achievement type."},
{"id": "badges.error.must_provide_type_id", "translation": "You must provide a type id"}, {"id": "badges.error.must_provide_type_id", "translation": "You must provide a type id"},
{"id": "badges.error.cannot_edit_type", "translation": "You cannot edit this type"}, {"id": "badges.error.cannot_edit_type", "translation": "You cannot edit this type"},
{"id": "badges.error.no_permissions_grant", "translation": "You have no permissions to grant this badge"}, {"id": "badges.error.no_permissions_grant", "translation": "You have no permissions to grant this achievement"},
{"id": "badges.error.cannot_grant_badge", "translation": "You cannot grant that badge"}, {"id": "badges.error.cannot_grant_badge", "translation": "You cannot grant that achievement"},
{"id": "badges.error.specify_subscription", "translation": "Specify what you want to do."}, {"id": "badges.error.specify_subscription", "translation": "Specify what you want to do."},
{"id": "badges.error.create_or_delete_subscription", "translation": "You can either create or delete subscriptions"}, {"id": "badges.error.create_or_delete_subscription", "translation": "You can either create or delete subscriptions"},
{"id": "badges.error.cannot_create_subscription", "translation": "You cannot create subscriptions"}, {"id": "badges.error.cannot_create_subscription", "translation": "You cannot create subscriptions"},
{"id": "badges.error.no_permissions_create_type", "translation": "You have no permissions to create a badge type."}, {"id": "badges.error.no_permissions_create_type", "translation": "You have no permissions to create an achievement type."},
{"id": "badges.error.already_owned", "translation": "This achievement is already owned by this user"},
{"id": "badges.success.clean", "translation": "Clean"}, {"id": "badges.success.clean", "translation": "Clean"},
{"id": "badges.success.granted", "translation": "Granted"}, {"id": "badges.success.granted", "translation": "Granted"},
@ -72,8 +73,8 @@
{"id": "badges.api.empty_emoji", "translation": "Empty emoji"}, {"id": "badges.api.empty_emoji", "translation": "Empty emoji"},
{"id": "badges.api.invalid_field", "translation": "Invalid field"}, {"id": "badges.api.invalid_field", "translation": "Invalid field"},
{"id": "badges.api.type_not_exist", "translation": "This type does not exist"}, {"id": "badges.api.type_not_exist", "translation": "This type does not exist"},
{"id": "badges.api.no_permissions_create_badge", "translation": "You have no permissions to create this badge"}, {"id": "badges.api.no_permissions_create_badge", "translation": "You have no permissions to create this achievement"},
{"id": "badges.api.badge_created", "translation": "Badge `%s` created."}, {"id": "badges.api.badge_created", "translation": "Achievement `%s` created."},
{"id": "badges.api.no_permissions_create_type", "translation": "You have no permissions to create a type"}, {"id": "badges.api.no_permissions_create_type", "translation": "You have no permissions to create a type"},
{"id": "badges.api.cannot_find_user", "translation": "Cannot find user"}, {"id": "badges.api.cannot_find_user", "translation": "Cannot find user"},
{"id": "badges.api.error_getting_user", "translation": "Error getting user %s: %v"}, {"id": "badges.api.error_getting_user", "translation": "Error getting user %s: %v"},
@ -83,15 +84,15 @@
{"id": "badges.api.could_not_get_type", "translation": "Could not get the type"}, {"id": "badges.api.could_not_get_type", "translation": "Could not get the type"},
{"id": "badges.api.no_permissions_edit_type", "translation": "You have no permissions to edit this type"}, {"id": "badges.api.no_permissions_edit_type", "translation": "You have no permissions to edit this type"},
{"id": "badges.api.type_updated", "translation": "Type `%s` updated."}, {"id": "badges.api.type_updated", "translation": "Type `%s` updated."},
{"id": "badges.api.cannot_get_badge", "translation": "Cannot get badge"}, {"id": "badges.api.cannot_get_badge", "translation": "Cannot get achievement"},
{"id": "badges.api.cannot_edit_badge", "translation": "You cannot edit this badge"}, {"id": "badges.api.cannot_edit_badge", "translation": "You cannot edit this achievement"},
{"id": "badges.api.could_not_get_badge", "translation": "Could not get the badge"}, {"id": "badges.api.could_not_get_badge", "translation": "Could not get the achievement"},
{"id": "badges.api.no_permissions_edit_badge", "translation": "You have no permissions to edit this badge"}, {"id": "badges.api.no_permissions_edit_badge", "translation": "You have no permissions to edit this achievement"},
{"id": "badges.api.badge_updated", "translation": "Badge `%s` updated."}, {"id": "badges.api.badge_updated", "translation": "Achievement `%s` updated."},
{"id": "badges.api.badge_not_found", "translation": "Badge not found"}, {"id": "badges.api.badge_not_found", "translation": "Achievement not found"},
{"id": "badges.api.no_permissions_grant", "translation": "You have no permissions to grant this badge"}, {"id": "badges.api.no_permissions_grant", "translation": "You have no permissions to grant this achievement"},
{"id": "badges.api.user_not_found", "translation": "User not found"}, {"id": "badges.api.user_not_found", "translation": "User not found"},
{"id": "badges.api.badge_granted", "translation": "Badge `%s` granted to @%s."}, {"id": "badges.api.badge_granted", "translation": "Achievement `%s` granted to @%s."},
{"id": "badges.api.cannot_create_subscription", "translation": "You cannot create a subscription"}, {"id": "badges.api.cannot_create_subscription", "translation": "You cannot create a subscription"},
{"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"},
@ -99,9 +100,9 @@
{"id": "badges.api.cannot_delete_default_type", "translation": "Cannot delete the default type"}, {"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` achievement."},
{"id": "badges.notify.dm_reason", "translation": "\nWhy? "}, {"id": "badges.notify.dm_reason", "translation": "\nWhy? "},
{"id": "badges.notify.title", "translation": "%sbadge granted!"}, {"id": "badges.notify.title", "translation": "%sachievement granted!"},
{"id": "badges.notify.channel_text", "translation": "@%s granted @%s the %s`%s` badge."}, {"id": "badges.notify.channel_text", "translation": "@%s granted @%s the %s`%s` achievement."},
{"id": "badges.notify.no_permission_channel", "translation": "You don't have permissions to notify the grant on this channel."} {"id": "badges.notify.no_permission_channel", "translation": "You don't have permissions to notify the grant on this channel."}
] ]

View File

@ -18,6 +18,7 @@ type Bundle i18n.Bundle
func Init() *Bundle { func Init() *Bundle {
bundle := i18n.NewBundle(language.Russian) bundle := i18n.NewBundle(language.Russian)
_, _ = bundle.LoadMessageFileFS(i18nFiles, "en.json") _, _ = bundle.LoadMessageFileFS(i18nFiles, "en.json")
_, _ = bundle.LoadMessageFileFS(i18nFiles, "ru.json")
return (*Bundle)(bundle) return (*Bundle)(bundle)
} }

View File

@ -1,21 +1,21 @@
[ [
{"id": "badges.dialog.create_badge.title", "translation": "Создать значок"}, {"id": "badges.dialog.create_badge.title", "translation": "Создать достижение"},
{"id": "badges.dialog.create_badge.submit", "translation": "Создать"}, {"id": "badges.dialog.create_badge.submit", "translation": "Создать"},
{"id": "badges.dialog.edit_badge.title", "translation": "Редактировать значок"}, {"id": "badges.dialog.edit_badge.title", "translation": "Редактировать достижение"},
{"id": "badges.dialog.edit_badge.submit", "translation": "Сохранить"}, {"id": "badges.dialog.edit_badge.submit", "translation": "Сохранить"},
{"id": "badges.dialog.create_type.title", "translation": "Создать тип"}, {"id": "badges.dialog.create_type.title", "translation": "Создать тип"},
{"id": "badges.dialog.create_type.submit", "translation": "Создать"}, {"id": "badges.dialog.create_type.submit", "translation": "Создать"},
{"id": "badges.dialog.edit_type.title", "translation": "Редактировать тип"}, {"id": "badges.dialog.edit_type.title", "translation": "Редактировать тип"},
{"id": "badges.dialog.edit_type.submit", "translation": "Сохранить"}, {"id": "badges.dialog.edit_type.submit", "translation": "Сохранить"},
{"id": "badges.dialog.grant.title", "translation": "Выдать значок"}, {"id": "badges.dialog.grant.title", "translation": "Выдать достижение"},
{"id": "badges.dialog.grant.submit", "translation": "Выдать"}, {"id": "badges.dialog.grant.submit", "translation": "Выдать"},
{"id": "badges.dialog.grant.intro", "translation": "Выдать значок пользователю @%s"}, {"id": "badges.dialog.grant.intro", "translation": "Выдать достижение пользователю @%s"},
{"id": "badges.dialog.create_subscription.title", "translation": "Создать подписку"}, {"id": "badges.dialog.create_subscription.title", "translation": "Создать подписку"},
{"id": "badges.dialog.create_subscription.submit", "translation": "Добавить"}, {"id": "badges.dialog.create_subscription.submit", "translation": "Добавить"},
{"id": "badges.dialog.create_subscription.intro", "translation": "Выберите тип значка, на который хотите подписать этот канал."}, {"id": "badges.dialog.create_subscription.intro", "translation": "Выберите тип достижения, на который хотите подписать этот канал."},
{"id": "badges.dialog.delete_subscription.title", "translation": "Удалить подписку"}, {"id": "badges.dialog.delete_subscription.title", "translation": "Удалить подписку"},
{"id": "badges.dialog.delete_subscription.submit", "translation": "Удалить"}, {"id": "badges.dialog.delete_subscription.submit", "translation": "Удалить"},
{"id": "badges.dialog.delete_subscription.intro", "translation": "Выберите тип значка, подписку на который хотите удалить из этого канала."}, {"id": "badges.dialog.delete_subscription.intro", "translation": "Выберите тип достижения, подписку на который хотите удалить из этого канала."},
{"id": "badges.field.name", "translation": "Название"}, {"id": "badges.field.name", "translation": "Название"},
{"id": "badges.field.description", "translation": "Описание"}, {"id": "badges.field.description", "translation": "Описание"},
@ -23,45 +23,46 @@
{"id": "badges.field.image.help", "translation": "Введите название эмодзи"}, {"id": "badges.field.image.help", "translation": "Введите название эмодзи"},
{"id": "badges.field.type", "translation": "Тип"}, {"id": "badges.field.type", "translation": "Тип"},
{"id": "badges.field.multiple", "translation": "Многократный"}, {"id": "badges.field.multiple", "translation": "Многократный"},
{"id": "badges.field.multiple.help", "translation": "Можно ли выдавать этот значок несколько раз"}, {"id": "badges.field.multiple.help", "translation": "Можно ли выдавать это достижение несколько раз"},
{"id": "badges.field.delete_badge", "translation": "Удалить значок"}, {"id": "badges.field.delete_badge", "translation": "Удалить достижение"},
{"id": "badges.field.delete_badge.help", "translation": "ВНИМАНИЕ: если отметить, значок будет удалён безвозвратно."}, {"id": "badges.field.delete_badge.help", "translation": "ВНИМАНИЕ: если отметить, достижение будет удалён безвозвратно."},
{"id": "badges.field.everyone_can_create", "translation": "Все могут создавать значки"}, {"id": "badges.field.everyone_can_create", "translation": "Все могут создавать достижения"},
{"id": "badges.field.everyone_can_create.help", "translation": "Любой пользователь может создать значок этого типа"}, {"id": "badges.field.everyone_can_create.help", "translation": "Любой пользователь может создать достижение этого типа"},
{"id": "badges.field.allowlist_create", "translation": "Список допущенных к созданию"}, {"id": "badges.field.allowlist_create", "translation": "Список допущенных к созданию"},
{"id": "badges.field.allowlist_create.help", "translation": "Укажите имена пользователей через запятую (,), которые могут создавать значки этого типа."}, {"id": "badges.field.allowlist_create.help", "translation": "Укажите имена пользователей через запятую (,), которые могут создавать достижения этого типа."},
{"id": "badges.field.everyone_can_grant", "translation": "Все могут выдавать значки"}, {"id": "badges.field.everyone_can_grant", "translation": "Все могут выдавать достижения"},
{"id": "badges.field.everyone_can_grant.help", "translation": "Любой пользователь может выдать значок этого типа"}, {"id": "badges.field.everyone_can_grant.help", "translation": "Любой пользователь может выдать достижение этого типа"},
{"id": "badges.field.allowlist_grant", "translation": "Список допущенных к выдаче"}, {"id": "badges.field.allowlist_grant", "translation": "Список допущенных к выдаче"},
{"id": "badges.field.allowlist_grant.help", "translation": "Укажите имена пользователей через запятую (,), которые могут выдавать значки этого типа."}, {"id": "badges.field.allowlist_grant.help", "translation": "Укажите имена пользователей через запятую (,), которые могут выдавать достижения этого типа."},
{"id": "badges.field.delete_type", "translation": "Удалить тип"}, {"id": "badges.field.delete_type", "translation": "Удалить тип"},
{"id": "badges.field.delete_type.help", "translation": "ВНИМАНИЕ: если отметить, этот тип и все связанные значки будут удалены безвозвратно."}, {"id": "badges.field.delete_type.help", "translation": "ВНИМАНИЕ: если отметить, этот тип и все связанные достижения будут удалены безвозвратно."},
{"id": "badges.field.user", "translation": "Пользователь"}, {"id": "badges.field.user", "translation": "Пользователь"},
{"id": "badges.field.badge", "translation": "Значок"}, {"id": "badges.field.badge", "translation": "Достижение"},
{"id": "badges.field.reason", "translation": "Причина"}, {"id": "badges.field.reason", "translation": "Причина"},
{"id": "badges.field.reason.help", "translation": "Причина выдачи значка. Будет видна пользователю и в уведомлениях о выдаче (например, в подписках)."}, {"id": "badges.field.reason.help", "translation": "Причина выдачи достижения. Будет видна пользователю и в уведомлениях о выдаче (например, в подписках)."},
{"id": "badges.field.notify_here", "translation": "Уведомить в этом канале"}, {"id": "badges.field.notify_here", "translation": "Уведомить в этом канале"},
{"id": "badges.field.notify_here.help", "translation": "Если отметить, бот отправит сообщение в этот канал о том, что вы выдали значок этому пользователю."}, {"id": "badges.field.notify_here.help", "translation": "Если отметить, бот отправит сообщение в этот канал о том, что вы выдали достижение этому пользователю."},
{"id": "badges.error.unknown", "translation": "Произошла неизвестная ошибка. Обратитесь к системному администратору."}, {"id": "badges.error.unknown", "translation": "Произошла неизвестная ошибка. Обратитесь к системному администратору."},
{"id": "badges.error.cannot_get_user", "translation": "Не удалось получить пользователя."}, {"id": "badges.error.cannot_get_user", "translation": "Не удалось получить пользователя."},
{"id": "badges.error.only_sysadmin_clean", "translation": "Только системный администратор может очистить базу значков."}, {"id": "badges.error.only_sysadmin_clean", "translation": "Только системный администратор может очистить базу достижений."},
{"id": "badges.error.specify_create", "translation": "Укажите, что вы хотите создать."}, {"id": "badges.error.specify_create", "translation": "Укажите, что вы хотите создать."},
{"id": "badges.error.create_badge_or_type", "translation": "Можно создать badge или type"}, {"id": "badges.error.create_badge_or_type", "translation": "Можно создать badge или type"},
{"id": "badges.error.no_types_available", "translation": "Вы не можете создать значки ни одного типа."}, {"id": "badges.error.no_types_available", "translation": "Вы не можете создать достижения ни одного типа."},
{"id": "badges.error.must_set_badge_id", "translation": "Необходимо указать ID значка"}, {"id": "badges.error.must_set_badge_id", "translation": "Необходимо указать ID достижения"},
{"id": "badges.error.cannot_edit_badge", "translation": "У вас нет прав на редактирование этого значка"}, {"id": "badges.error.cannot_edit_badge", "translation": "У вас нет прав на редактирование этого достижения"},
{"id": "badges.error.specify_edit", "translation": "Укажите, что вы хотите отредактировать."}, {"id": "badges.error.specify_edit", "translation": "Укажите, что вы хотите отредактировать."},
{"id": "badges.error.edit_badge_or_type", "translation": "Можно редактировать badge или type"}, {"id": "badges.error.edit_badge_or_type", "translation": "Можно редактировать badge или type"},
{"id": "badges.error.no_permissions_edit_type", "translation": "У вас нет прав на редактирование типа значков."}, {"id": "badges.error.no_permissions_edit_type", "translation": "У вас нет прав на редактирование типа достижений."},
{"id": "badges.error.must_provide_type_id", "translation": "Необходимо указать ID типа"}, {"id": "badges.error.must_provide_type_id", "translation": "Необходимо указать ID типа"},
{"id": "badges.error.cannot_edit_type", "translation": "У вас нет прав на редактирование этого типа"}, {"id": "badges.error.cannot_edit_type", "translation": "У вас нет прав на редактирование этого типа"},
{"id": "badges.error.no_permissions_grant", "translation": "У вас нет прав на выдачу этого значка"}, {"id": "badges.error.no_permissions_grant", "translation": "У вас нет прав на выдачу этого достижения"},
{"id": "badges.error.cannot_grant_badge", "translation": "Вы не можете выдать этот значок"}, {"id": "badges.error.cannot_grant_badge", "translation": "Вы не можете выдать это достижение"},
{"id": "badges.error.specify_subscription", "translation": "Укажите, что вы хотите сделать."}, {"id": "badges.error.specify_subscription", "translation": "Укажите, что вы хотите сделать."},
{"id": "badges.error.create_or_delete_subscription", "translation": "Можно создать или удалить подписку"}, {"id": "badges.error.create_or_delete_subscription", "translation": "Можно создать или удалить подписку"},
{"id": "badges.error.cannot_create_subscription", "translation": "Вы не можете создавать подписки"}, {"id": "badges.error.cannot_create_subscription", "translation": "Вы не можете создавать подписки"},
{"id": "badges.error.no_permissions_create_type", "translation": "У вас нет прав на создание типа значков."}, {"id": "badges.error.no_permissions_create_type", "translation": "У вас нет прав на создание типа достижений."},
{"id": "badges.error.already_owned", "translation": "Это достижение уже выдано этому пользователю"},
{"id": "badges.success.clean", "translation": "Очищено"}, {"id": "badges.success.clean", "translation": "Очищено"},
{"id": "badges.success.granted", "translation": "Выдано"}, {"id": "badges.success.granted", "translation": "Выдано"},
@ -72,8 +73,8 @@
{"id": "badges.api.empty_emoji", "translation": "Пустой эмодзи"}, {"id": "badges.api.empty_emoji", "translation": "Пустой эмодзи"},
{"id": "badges.api.invalid_field", "translation": "Некорректное поле"}, {"id": "badges.api.invalid_field", "translation": "Некорректное поле"},
{"id": "badges.api.type_not_exist", "translation": "Этот тип не существует"}, {"id": "badges.api.type_not_exist", "translation": "Этот тип не существует"},
{"id": "badges.api.no_permissions_create_badge", "translation": "У вас нет прав на создание этого значка"}, {"id": "badges.api.no_permissions_create_badge", "translation": "У вас нет прав на создание этого достижения"},
{"id": "badges.api.badge_created", "translation": "Значок `%s` создан."}, {"id": "badges.api.badge_created", "translation": "Достижение `%s` создано."},
{"id": "badges.api.no_permissions_create_type", "translation": "У вас нет прав на создание типа"}, {"id": "badges.api.no_permissions_create_type", "translation": "У вас нет прав на создание типа"},
{"id": "badges.api.cannot_find_user", "translation": "Не удалось найти пользователя"}, {"id": "badges.api.cannot_find_user", "translation": "Не удалось найти пользователя"},
{"id": "badges.api.error_getting_user", "translation": "Ошибка получения пользователя %s: %v"}, {"id": "badges.api.error_getting_user", "translation": "Ошибка получения пользователя %s: %v"},
@ -83,15 +84,15 @@
{"id": "badges.api.could_not_get_type", "translation": "Не удалось получить тип"}, {"id": "badges.api.could_not_get_type", "translation": "Не удалось получить тип"},
{"id": "badges.api.no_permissions_edit_type", "translation": "У вас нет прав на редактирование этого типа"}, {"id": "badges.api.no_permissions_edit_type", "translation": "У вас нет прав на редактирование этого типа"},
{"id": "badges.api.type_updated", "translation": "Тип `%s` обновлён."}, {"id": "badges.api.type_updated", "translation": "Тип `%s` обновлён."},
{"id": "badges.api.cannot_get_badge", "translation": "Не удалось получить значок"}, {"id": "badges.api.cannot_get_badge", "translation": "Не удалось получить достижение"},
{"id": "badges.api.cannot_edit_badge", "translation": "Вы не можете редактировать этот значок"}, {"id": "badges.api.cannot_edit_badge", "translation": "Вы не можете редактировать это достижение"},
{"id": "badges.api.could_not_get_badge", "translation": "Не удалось получить значок"}, {"id": "badges.api.could_not_get_badge", "translation": "Не удалось получить достижение"},
{"id": "badges.api.no_permissions_edit_badge", "translation": "У вас нет прав на редактирование этого значка"}, {"id": "badges.api.no_permissions_edit_badge", "translation": "У вас нет прав на редактирование этого достижения"},
{"id": "badges.api.badge_updated", "translation": "Значок `%s` обновлён."}, {"id": "badges.api.badge_updated", "translation": "Достижение `%s` обновлёно."},
{"id": "badges.api.badge_not_found", "translation": "Значок не найден"}, {"id": "badges.api.badge_not_found", "translation": "Достижение не найдено"},
{"id": "badges.api.no_permissions_grant", "translation": "У вас нет прав на выдачу этого значка"}, {"id": "badges.api.no_permissions_grant", "translation": "У вас нет прав на выдачу этого достижения"},
{"id": "badges.api.user_not_found", "translation": "Пользователь не найден"}, {"id": "badges.api.user_not_found", "translation": "Пользователь не найден"},
{"id": "badges.api.badge_granted", "translation": "Значок `%s` выдан @%s."}, {"id": "badges.api.badge_granted", "translation": "Достижение `%s` выдан @%s."},
{"id": "badges.api.cannot_create_subscription", "translation": "Вы не можете создать подписку"}, {"id": "badges.api.cannot_create_subscription", "translation": "Вы не можете создать подписку"},
{"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": "Вы не можете удалить подписку"},
@ -99,9 +100,9 @@
{"id": "badges.api.cannot_delete_default_type", "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`."},
{"id": "badges.notify.dm_reason", "translation": "\nПочему? "}, {"id": "badges.notify.dm_reason", "translation": "\nПочему? "},
{"id": "badges.notify.title", "translation": "%sзначок выдан!"}, {"id": "badges.notify.title", "translation": "%sдостижение выдано!"},
{"id": "badges.notify.channel_text", "translation": "@%s выдал @%s значок %s`%s`."}, {"id": "badges.notify.channel_text", "translation": "@%s выдал @%s достижение %s`%s`."},
{"id": "badges.notify.no_permission_channel", "translation": "У вас нет прав на отправку уведомления о выдаче в этот канал."} {"id": "badges.notify.no_permission_channel", "translation": "У вас нет прав на отправку уведомления о выдаче в этот канал."}
] ]

View File

@ -12,6 +12,7 @@ import (
var errInvalidBadge = errors.New("invalid badge") var errInvalidBadge = errors.New("invalid badge")
var errBadgeNotFound = errors.New("badge not found") var errBadgeNotFound = errors.New("badge not found")
var errAlreadyOwned = errors.New("already owned")
type Store interface { type Store interface {
// Interface // Interface
@ -33,6 +34,8 @@ type Store interface {
UpdateBadge(b *badgesmodel.Badge) error UpdateBadge(b *badgesmodel.Badge) error
DeleteType(tID badgesmodel.BadgeType) error DeleteType(tID badgesmodel.BadgeType) error
DeleteBadge(bID badgesmodel.BadgeID) error DeleteBadge(bID badgesmodel.BadgeID) error
RevokeOwnership(badgeID badgesmodel.BadgeID, userID string, grantTime string) error
FindOwnership(badgeID badgesmodel.BadgeID, userID string, grantTime string) (*badgesmodel.Ownership, error)
AddSubscription(tID badgesmodel.BadgeType, cID string) error AddSubscription(tID badgesmodel.BadgeType, cID string) error
RemoveSubscriptions(tID badgesmodel.BadgeType, cID string) error RemoveSubscriptions(tID badgesmodel.BadgeType, cID string) error
@ -503,6 +506,25 @@ func (s *store) AddSubscription(tID badgesmodel.BadgeType, cID string) error {
return s.doAtomic(func() (bool, error) { return s.atomicAddSubscription(toAdd) }) return s.doAtomic(func() (bool, error) { return s.atomicAddSubscription(toAdd) })
} }
func (s *store) FindOwnership(badgeID badgesmodel.BadgeID, userID string, grantTime string) (*badgesmodel.Ownership, error) {
ownership, _, err := s.getOwnershipList()
if err != nil {
return nil, err
}
for _, o := range ownership {
if o.Badge == badgeID && o.User == userID && o.Time.Format(time.RFC3339Nano) == grantTime {
return &o, nil
}
}
return nil, errors.New("ownership not found")
}
func (s *store) RevokeOwnership(badgeID badgesmodel.BadgeID, userID string, grantTime string) error {
return s.doAtomic(func() (bool, error) { return s.atomicRevokeOwnership(badgeID, userID, grantTime) })
}
func (s *store) RemoveSubscriptions(tID badgesmodel.BadgeType, cID string) error { func (s *store) RemoveSubscriptions(tID badgesmodel.BadgeType, cID string) error {
toRemove := badgesmodel.Subscription{ChannelID: cID, TypeID: tID} toRemove := badgesmodel.Subscription{ChannelID: cID, TypeID: tID}
return s.doAtomic(func() (bool, error) { return s.atomicRemoveSubscription(toRemove) }) return s.doAtomic(func() (bool, error) { return s.atomicRemoveSubscription(toRemove) })

View File

@ -3,6 +3,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"time"
"github.com/larkox/mattermost-plugin-badges/badgesmodel" "github.com/larkox/mattermost-plugin-badges/badgesmodel"
) )
@ -107,7 +108,7 @@ func (s *store) atomicAddBadgeToOwnership(o badgesmodel.Ownership, isMultiple bo
} }
if !isMultiple && ownership.IsOwned(o.User, o.Badge) { if !isMultiple && ownership.IsOwned(o.User, o.Badge) {
return false, true, nil return false, true, errAlreadyOwned
} }
ownership = append(ownership, o) ownership = append(ownership, o)
@ -159,6 +160,28 @@ func (s *store) atomicUpdateBadge(b *badgesmodel.Badge) (bool, error) {
return s.compareAndSet(KVKeyBadges, data, bb) return s.compareAndSet(KVKeyBadges, data, bb)
} }
func (s *store) atomicRevokeOwnership(badgeID badgesmodel.BadgeID, userID string, grantTime string) (bool, error) {
ownership, data, err := s.getOwnershipList()
if err != nil {
return false, err
}
found := false
for i, o := range ownership {
if o.Badge == badgeID && o.User == userID && o.Time.Format(time.RFC3339Nano) == grantTime {
ownership = append(ownership[:i], ownership[i+1:]...)
found = true
break
}
}
if !found {
return true, nil
}
return s.compareAndSet(KVKeyOwnership, data, ownership)
}
func (s *store) atomicAddSubscription(toAdd badgesmodel.Subscription) (bool, error) { func (s *store) atomicAddSubscription(toAdd badgesmodel.Subscription) (bool, error) {
subs, data, err := s.getAllSubscriptions() subs, data, err := s.getAllSubscriptions()
if err != nil { if err != nil {

View File

@ -1,16 +1,16 @@
{ {
"badges.loading": "Loading...", "badges.loading": "Loading...",
"badges.no_badges_yet": "No badges yet.", "badges.no_badges_yet": "No achievements yet.",
"badges.empty.title": "No badges yet", "badges.empty.title": "No achievements yet",
"badges.empty.description": "Create your first badge to recognize achievements and contributions of your team members.", "badges.empty.description": "Create your first achievement to recognize contributions of your team members.",
"badges.badge_not_found": "Badge not found.", "badges.badge_not_found": "Achievement not found.",
"badges.user_not_found": "User not found.", "badges.user_not_found": "User not found.",
"badges.unknown": "unknown", "badges.unknown": "unknown",
"badges.rhs.all_badges": "All badges", "badges.rhs.all_badges": "All achievements",
"badges.rhs.my_badges": "My badges", "badges.rhs.my_badges": "My achievements",
"badges.rhs.user_badges": "@{username}'s badges", "badges.rhs.user_badges": "@{username}'s achievements",
"badges.rhs.badge_details": "Badge Details", "badges.rhs.badge_details": "Achievement Details",
"badges.label.name": "Name:", "badges.label.name": "Name:",
"badges.label.description": "Description:", "badges.label.description": "Description:",
@ -27,55 +27,56 @@
"badges.granted_to": "Granted to:", "badges.granted_to": "Granted to:",
"badges.not_granted_yet": "Not granted to anyone yet", "badges.not_granted_yet": "Not granted to anyone yet",
"badges.set_status": "Set status to this badge", "badges.set_status": "Set status to this achievement",
"badges.grant_badge": "Grant badge", "badges.grant_badge": "Grant achievement",
"badges.and_more": "and {count} more. Click to see all.", "badges.and_more": "and {count} more. Click to see all.",
"badges.menu.open_list": "Open the list of all badges.", "badges.menu.open_list": "Open the list of all achievements.",
"badges.menu.create_badge": "Create badge", "badges.menu.create_badge": "Create achievement",
"badges.menu.create_type": "Create badge type", "badges.menu.create_type": "Create achievement type",
"badges.menu.add_subscription": "Add badge subscription", "badges.menu.add_subscription": "Add achievement subscription",
"badges.menu.remove_subscription": "Remove badge subscription", "badges.menu.remove_subscription": "Remove achievement subscription",
"badges.sidebar.title": "Badges", "badges.sidebar.title": "Achievements",
"badges.popover.title": "Badges", "badges.popover.title": "Achievements",
"badges.admin.label": "Achievements Administrators:", "badges.admin.label": "Achievements Administrators:",
"badges.admin.placeholder": "Start typing a name...", "badges.admin.placeholder": "Start typing a name...",
"badges.admin.help_text": "These users will be considered achievements plugin administrators. They can create types, as well as modify and grant any badges.", "badges.admin.help_text": "These users will be considered achievements plugin administrators. They can create types, as well as modify and grant any achievements.",
"badges.admin.no_results": "No users found", "badges.admin.no_results": "No users found",
"badges.rhs.create_badge": "+ Create badge", "badges.rhs.create_badge": "+ Create achievement",
"badges.rhs.edit_badge": "Edit", "badges.rhs.edit_badge": "Edit",
"badges.rhs.types": "Types", "badges.rhs.types": "Types",
"badges.rhs.create_type": "+ Create type", "badges.rhs.create_type": "+ Create type",
"badges.modal.create_badge_title": "Create Badge", "badges.modal.create_badge_title": "Create Achievement",
"badges.modal.edit_badge_title": "Edit Badge", "badges.modal.edit_badge_title": "Edit Achievement",
"badges.modal.field_name": "Name", "badges.modal.field_name": "Name",
"badges.modal.field_name_placeholder": "Badge name (max 20 chars)", "badges.modal.field_name_placeholder": "Achievement name (max 20 chars)",
"badges.modal.field_description": "Description", "badges.modal.field_description": "Description",
"badges.modal.field_description_placeholder": "Badge description (max 120 chars)", "badges.modal.field_description_placeholder": "Achievement description (max 120 chars)",
"badges.modal.field_image": "Emoji", "badges.modal.field_image": "Emoji",
"badges.modal.field_image_placeholder": "Emoji name (e.g. star)", "badges.modal.field_image_placeholder": "Emoji name (e.g. star)",
"badges.modal.field_type": "Type", "badges.modal.field_type": "Type",
"badges.modal.field_type_placeholder": "Select badge type", "badges.modal.field_type_placeholder": "Select achievement type",
"badges.modal.field_multiple": "Can be granted multiple times", "badges.modal.field_multiple": "Can be granted multiple times",
"badges.modal.create_new_type": "+ Create new type", "badges.modal.create_new_type": "+ Create new type",
"badges.modal.new_type_name": "Type name", "badges.modal.new_type_name": "Type name",
"badges.modal.new_type_name_placeholder": "Type name (max 20 chars)", "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_create": "Everyone can create achievements",
"badges.modal.new_type_everyone_grant": "Everyone can grant badges", "badges.modal.new_type_everyone_grant": "Everyone can grant achievements",
"badges.modal.btn_cancel": "Cancel", "badges.modal.btn_cancel": "Cancel",
"badges.modal.btn_create": "Create", "badges.modal.btn_create": "Create",
"badges.modal.btn_save": "Save", "badges.modal.btn_save": "Save",
"badges.modal.btn_creating": "Saving...", "badges.modal.btn_creating": "Saving...",
"badges.modal.btn_delete": "Delete badge", "badges.modal.btn_delete": "Delete achievement",
"badges.modal.btn_confirm_delete": "Yes, delete", "badges.modal.btn_confirm_delete": "Yes, delete",
"badges.modal.confirm_delete": "Are you sure?", "badges.modal.confirm_delete": "Are you sure?",
"badges.modal.confirm_delete_badge": "Delete achievement \"{name}\"?",
"badges.modal.error_generic": "An error occurred", "badges.modal.error_generic": "An error occurred",
"badges.modal.error_type_name_required": "Enter type name", "badges.modal.error_type_name_required": "Enter type name",
"badges.modal.error_type_required": "Select badge type", "badges.modal.error_type_required": "Select achievement type",
"badges.modal.create_type_title": "Create Type", "badges.modal.create_type_title": "Create Type",
"badges.modal.edit_type_title": "Edit Type", "badges.modal.edit_type_title": "Edit Type",
"badges.modal.btn_delete_type": "Delete type", "badges.modal.btn_delete_type": "Delete type",
@ -83,65 +84,73 @@
"badges.modal.confirm_delete_type": "Delete type \"{name}\"?", "badges.modal.confirm_delete_type": "Delete type \"{name}\"?",
"badges.modal.btn_confirm_delete_type": "Yes, delete", "badges.modal.btn_confirm_delete_type": "Yes, delete",
"badges.types.badge_count": "{count, plural, one {# badge} other {# badges}}", "badges.types.badge_count": "{count, plural, one {# achievement} other {# achievements}}",
"badges.types.everyone_can_create": "Everyone creates", "badges.types.everyone_can_create": "Everyone creates",
"badges.types.everyone_can_grant": "Everyone grants", "badges.types.everyone_can_grant": "Everyone grants",
"badges.types.is_default": "Default", "badges.types.is_default": "Default",
"badges.types.confirm_delete": "Delete type \"{name}\" and all its badges?", "badges.types.confirm_delete": "Delete type \"{name}\" and all its achievements?",
"badges.types.empty": "No types yet", "badges.types.empty": "No types yet",
"badges.types.no_badges": "No badges in this type", "badges.types.no_badges": "No achievements in this type",
"badges.rhs.back_to_types": "Back to types", "badges.rhs.back_to_types": "Back to types",
"badges.modal.allowlist_create": "Allowlist for creation", "badges.modal.allowlist_create": "Allowlist for creation",
"badges.modal.allowlist_create_help": "Users who can create badges of this type.", "badges.modal.allowlist_create_help": "Users who can create achievements of this type.",
"badges.modal.allowlist_grant": "Allowlist for granting", "badges.modal.allowlist_grant": "Allowlist for granting",
"badges.modal.allowlist_grant_help": "Users who can grant badges of this type.", "badges.modal.allowlist_grant_help": "Users who can grant achievements of this type.",
"badges.modal.allowlist_placeholder": "user-1, user-2, user-3", "badges.modal.allowlist_placeholder": "user-1, user-2, user-3",
"badges.grant.title": "Grant Badge", "badges.grant.title": "Grant Achievement",
"badges.grant.intro": "Grant badge to @{username}", "badges.grant.intro": "Grant achievement to @{username}",
"badges.grant.field_badge": "Badge", "badges.grant.field_badge": "Achievement",
"badges.grant.field_badge_placeholder": "Select a badge", "badges.grant.field_badge_placeholder": "Select an achievement",
"badges.grant.no_badges": "No badges available", "badges.grant.no_badges": "No achievements available",
"badges.grant.field_reason": "Reason", "badges.grant.field_reason": "Reason",
"badges.grant.field_reason_placeholder": "Why is this badge being granted? (optional)", "badges.grant.field_reason_placeholder": "Why is this achievement being granted? (optional)",
"badges.grant.notify_here": "Notify in channel", "badges.grant.notify_here": "Notify in channel",
"badges.grant.btn_grant": "Grant", "badges.grant.btn_grant": "Grant",
"badges.revoke.btn": "Revoke",
"badges.revoke.confirm": "Revoke achievement?",
"badges.revoke.confirm_yes": "Yes",
"badges.subscription.title_create": "Add Subscription", "badges.subscription.title_create": "Add Subscription",
"badges.subscription.title_delete": "Remove Subscription", "badges.subscription.title_delete": "Remove Subscription",
"badges.subscription.field_type": "Badge Type", "badges.subscription.field_type": "Achievement Type",
"badges.subscription.field_type_placeholder": "Select badge type", "badges.subscription.field_type_placeholder": "Select achievement type",
"badges.subscription.no_types": "No types available", "badges.subscription.no_types": "No types available",
"badges.subscription.btn_create": "Add", "badges.subscription.btn_create": "Add",
"badges.subscription.btn_delete": "Remove", "badges.subscription.btn_delete": "Remove",
"badges.error.invalid_badge_id": "Badge not specified", "badges.error.invalid_badge_id": "Achievement not specified",
"badges.error.invalid_user_id": "User not specified", "badges.error.invalid_user_id": "User not specified",
"badges.error.no_permission_grant": "Insufficient permissions to grant this badge", "badges.error.no_permission_grant": "Insufficient permissions to grant this achievement",
"badges.error.cannot_grant_badge": "Failed to grant badge", "badges.error.cannot_grant_badge": "Failed to grant achievement",
"badges.error.user_not_found": "User not found", "badges.error.user_not_found": "User not found",
"badges.error.invalid_type_id": "Badge type not specified", "badges.error.invalid_type_id": "Achievement type not specified",
"badges.error.no_permission_subscription": "Insufficient permissions to manage subscriptions", "badges.error.no_permission_subscription": "Insufficient permissions to manage subscriptions",
"badges.error.cannot_create_subscription": "Failed to create subscription", "badges.error.cannot_create_subscription": "Failed to create subscription",
"badges.error.cannot_delete_subscription": "Failed to delete subscription", "badges.error.cannot_delete_subscription": "Failed to delete subscription",
"badges.error.ownership_not_found": "Ownership not found",
"badges.error.no_permission_revoke": "Insufficient permissions to revoke",
"badges.error.cannot_revoke": "Failed to revoke",
"badges.error.already_owned": "This achievement is already owned by this user",
"badges.error.unknown": "An error occurred", "badges.error.unknown": "An error occurred",
"badges.error.cannot_get_user": "Failed to get user data", "badges.error.cannot_get_user": "Failed to get user data",
"badges.error.cannot_get_types": "Failed to load types", "badges.error.cannot_get_types": "Failed to load types",
"badges.error.cannot_get_badges": "Failed to load badges", "badges.error.cannot_get_badges": "Failed to load achievements",
"badges.error.invalid_request": "Invalid request format", "badges.error.invalid_request": "Invalid request format",
"badges.error.invalid_name": "Name is required", "badges.error.invalid_name": "Name is required",
"badges.error.invalid_image": "Emoji is required", "badges.error.invalid_image": "Emoji is required",
"badges.error.type_not_found": "Badge type not found", "badges.error.type_not_found": "Achievement type not found",
"badges.error.badge_not_found": "Badge not found", "badges.error.badge_not_found": "Achievement not found",
"badges.error.no_permission": "Insufficient permissions", "badges.error.no_permission": "Insufficient permissions",
"badges.error.missing_badge_id": "Badge ID is missing", "badges.error.missing_badge_id": "Achievement ID is missing",
"badges.error.missing_type_id": "Type 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_badge": "Failed to create achievement",
"badges.error.cannot_create_type": "Failed to create type", "badges.error.cannot_create_type": "Failed to create type",
"badges.error.cannot_update_badge": "Failed to update badge", "badges.error.cannot_update_badge": "Failed to update achievement",
"badges.error.cannot_delete_badge": "Failed to delete badge", "badges.error.cannot_delete_badge": "Failed to delete achievement",
"badges.error.cannot_update_type": "Failed to update type", "badges.error.cannot_update_type": "Failed to update type",
"badges.error.cannot_delete_type": "Failed to delete type", "badges.error.cannot_delete_type": "Failed to delete type",

View File

@ -1,16 +1,16 @@
{ {
"badges.loading": "Загрузка...", "badges.loading": "Загрузка...",
"badges.no_badges_yet": "Значков пока нет.", "badges.no_badges_yet": "Достижений пока нет.",
"badges.empty.title": "Значков пока нет", "badges.empty.title": "Достижений пока нет",
"badges.empty.description": "Создайте первый значок, чтобы отмечать достижения и заслуги участников команды.", "badges.empty.description": "Создайте первое достижение, чтобы отмечать заслуги участников команды.",
"badges.badge_not_found": "Значок не найден.", "badges.badge_not_found": "Достижение не найдено.",
"badges.user_not_found": "Пользователь не найден.", "badges.user_not_found": "Пользователь не найден.",
"badges.unknown": "неизвестно", "badges.unknown": "неизвестно",
"badges.rhs.all_badges": "Все значки", "badges.rhs.all_badges": "Все достижения",
"badges.rhs.my_badges": "Мои значки", "badges.rhs.my_badges": "Мои достижения",
"badges.rhs.user_badges": "Значки @{username}", "badges.rhs.user_badges": "Достижения @{username}",
"badges.rhs.badge_details": "Детали значка", "badges.rhs.badge_details": "Детали достижения",
"badges.label.name": "Название:", "badges.label.name": "Название:",
"badges.label.description": "Описание:", "badges.label.description": "Описание:",
@ -28,54 +28,55 @@
"badges.not_granted_yet": "Ещё никому не выдан", "badges.not_granted_yet": "Ещё никому не выдан",
"badges.set_status": "Установить как статус", "badges.set_status": "Установить как статус",
"badges.grant_badge": "Выдать значок", "badges.grant_badge": "Выдать достижение",
"badges.and_more": "и ещё {count}. Нажмите, чтобы увидеть все.", "badges.and_more": "и ещё {count}. Нажмите, чтобы увидеть все.",
"badges.menu.open_list": "Открыть список всех значков.", "badges.menu.open_list": "Открыть список всех достижений.",
"badges.menu.create_badge": "Создать значок", "badges.menu.create_badge": "Создать достижение",
"badges.menu.create_type": "Создать тип значков", "badges.menu.create_type": "Создать тип достижений",
"badges.menu.add_subscription": "Добавить подписку на значки", "badges.menu.add_subscription": "Добавить подписку на достижения",
"badges.menu.remove_subscription": "Удалить подписку на значки", "badges.menu.remove_subscription": "Удалить подписку на достижения",
"badges.sidebar.title": "Значки", "badges.sidebar.title": "Достижения",
"badges.popover.title": "Значки", "badges.popover.title": "Достижения",
"badges.admin.label": "Администраторы достижений:", "badges.admin.label": "Администраторы достижений:",
"badges.admin.placeholder": "Начните вводить имя...", "badges.admin.placeholder": "Начните вводить имя...",
"badges.admin.help_text": "Эти пользователи будут считаться администраторами плагина достижений. Они могут создавать типы, а также изменять и выдавать любые значки.", "badges.admin.help_text": "Эти пользователи будут считаться администраторами плагина достижений. Они могут создавать типы, а также изменять и выдавать любые достижения.",
"badges.admin.no_results": "Пользователь не найден", "badges.admin.no_results": "Пользователь не найден",
"badges.rhs.create_badge": "+ Создать значок", "badges.rhs.create_badge": "+ Создать достижение",
"badges.rhs.edit_badge": "Редактировать", "badges.rhs.edit_badge": "Редактировать",
"badges.rhs.types": "Типы", "badges.rhs.types": "Типы",
"badges.rhs.create_type": "+ Создать тип", "badges.rhs.create_type": "+ Создать тип",
"badges.modal.create_badge_title": "Создать значок", "badges.modal.create_badge_title": "Создать достижение",
"badges.modal.edit_badge_title": "Редактировать значок", "badges.modal.edit_badge_title": "Редактировать достижение",
"badges.modal.field_name": "Название", "badges.modal.field_name": "Название",
"badges.modal.field_name_placeholder": "Название значка (макс. 20 символов)", "badges.modal.field_name_placeholder": "Название достижения (макс. 20 символов)",
"badges.modal.field_description": "Описание", "badges.modal.field_description": "Описание",
"badges.modal.field_description_placeholder": "Описание значка (макс. 120 символов)", "badges.modal.field_description_placeholder": "Описание достижения (макс. 120 символов)",
"badges.modal.field_image": "Эмодзи", "badges.modal.field_image": "Эмодзи",
"badges.modal.field_image_placeholder": "Название эмодзи (напр. star)", "badges.modal.field_image_placeholder": "Название эмодзи (напр. star)",
"badges.modal.field_type": "Тип", "badges.modal.field_type": "Тип",
"badges.modal.field_type_placeholder": "Выберите тип значка", "badges.modal.field_type_placeholder": "Выберите тип достижения",
"badges.modal.field_multiple": "Можно выдавать несколько раз", "badges.modal.field_multiple": "Можно выдавать несколько раз",
"badges.modal.create_new_type": "+ Создать новый тип", "badges.modal.create_new_type": "+ Создать новый тип",
"badges.modal.new_type_name": "Название типа", "badges.modal.new_type_name": "Название типа",
"badges.modal.new_type_name_placeholder": "Название типа (макс. 20 символов)", "badges.modal.new_type_name_placeholder": "Название типа (макс. 20 символов)",
"badges.modal.new_type_everyone_create": "Все могут создавать значки", "badges.modal.new_type_everyone_create": "Все могут создавать достижения",
"badges.modal.new_type_everyone_grant": "Все могут выдавать значки", "badges.modal.new_type_everyone_grant": "Все могут выдавать достижения",
"badges.modal.btn_cancel": "Отмена", "badges.modal.btn_cancel": "Отмена",
"badges.modal.btn_create": "Создать", "badges.modal.btn_create": "Создать",
"badges.modal.btn_save": "Сохранить", "badges.modal.btn_save": "Сохранить",
"badges.modal.btn_creating": "Сохранение...", "badges.modal.btn_creating": "Сохранение...",
"badges.modal.btn_delete": "Удалить значок", "badges.modal.btn_delete": "Удалить достижение",
"badges.modal.btn_confirm_delete": "Да, удалить", "badges.modal.btn_confirm_delete": "Да, удалить",
"badges.modal.confirm_delete": "Вы уверены?", "badges.modal.confirm_delete": "Вы уверены?",
"badges.modal.confirm_delete_badge": "Удалить достижение «{name}»?",
"badges.modal.error_generic": "Произошла ошибка", "badges.modal.error_generic": "Произошла ошибка",
"badges.modal.error_type_name_required": "Введите название типа", "badges.modal.error_type_name_required": "Введите название типа",
"badges.modal.error_type_required": "Выберите тип значка", "badges.modal.error_type_required": "Выберите тип достижения",
"badges.modal.create_type_title": "Создать тип", "badges.modal.create_type_title": "Создать тип",
"badges.modal.edit_type_title": "Редактировать тип", "badges.modal.edit_type_title": "Редактировать тип",
"badges.modal.btn_delete_type": "Удалить тип", "badges.modal.btn_delete_type": "Удалить тип",
@ -83,65 +84,73 @@
"badges.modal.confirm_delete_type": "Удалить тип «{name}»?", "badges.modal.confirm_delete_type": "Удалить тип «{name}»?",
"badges.modal.btn_confirm_delete_type": "Да, удалить", "badges.modal.btn_confirm_delete_type": "Да, удалить",
"badges.types.badge_count": "{count, plural, one {# значок} few {# значка} many {# значков} other {# значков}}", "badges.types.badge_count": "{count, plural, one {# достижение} few {# достижения} many {# достижений} other {# достижений}}",
"badges.types.everyone_can_create": "Все создают", "badges.types.everyone_can_create": "Все создают",
"badges.types.everyone_can_grant": "Все выдают", "badges.types.everyone_can_grant": "Все выдают",
"badges.types.is_default": "По умолчанию", "badges.types.is_default": "По умолчанию",
"badges.types.confirm_delete": "Удалить тип «{name}» и все его значки?", "badges.types.confirm_delete": "Удалить тип «{name}» и все его достижения?",
"badges.types.empty": "Типов пока нет", "badges.types.empty": "Типов пока нет",
"badges.types.no_badges": "В этом типе нет значков", "badges.types.no_badges": "В этом типе нет достижений",
"badges.rhs.back_to_types": "Назад к типам", "badges.rhs.back_to_types": "Назад к типам",
"badges.modal.allowlist_create": "Список допущенных к созданию", "badges.modal.allowlist_create": "Список допущенных к созданию",
"badges.modal.allowlist_create_help": "Пользователи, которые могут создавать значки этого типа.", "badges.modal.allowlist_create_help": "Пользователи, которые могут создавать достижения этого типа.",
"badges.modal.allowlist_grant": "Список допущенных к выдаче", "badges.modal.allowlist_grant": "Список допущенных к выдаче",
"badges.modal.allowlist_grant_help": "Пользователи, которые могут выдавать значки этого типа.", "badges.modal.allowlist_grant_help": "Пользователи, которые могут выдавать достижения этого типа.",
"badges.modal.allowlist_placeholder": "user-1, user-2, user-3", "badges.modal.allowlist_placeholder": "user-1, user-2, user-3",
"badges.grant.title": "Выдать значок", "badges.grant.title": "Выдать достижение",
"badges.grant.intro": "Выдать значок пользователю @{username}", "badges.grant.intro": "Выдать достижение пользователю @{username}",
"badges.grant.field_badge": "Значок", "badges.grant.field_badge": "Достижение",
"badges.grant.field_badge_placeholder": "Выберите значок", "badges.grant.field_badge_placeholder": "Выберите достижение",
"badges.grant.no_badges": "Нет доступных значков", "badges.grant.no_badges": "Нет доступных достижений",
"badges.grant.field_reason": "Причина", "badges.grant.field_reason": "Причина",
"badges.grant.field_reason_placeholder": "За что выдаётся значок? (необязательно)", "badges.grant.field_reason_placeholder": "За что выдаётся достижение? (необязательно)",
"badges.grant.notify_here": "Уведомить в канале", "badges.grant.notify_here": "Уведомить в канале",
"badges.grant.btn_grant": "Выдать", "badges.grant.btn_grant": "Выдать",
"badges.revoke.btn": "Снять достижение",
"badges.revoke.confirm": "Снять достижение?",
"badges.revoke.confirm_yes": "Да",
"badges.subscription.title_create": "Добавить подписку", "badges.subscription.title_create": "Добавить подписку",
"badges.subscription.title_delete": "Удалить подписку", "badges.subscription.title_delete": "Удалить подписку",
"badges.subscription.field_type": "Тип значков", "badges.subscription.field_type": "Тип достижений",
"badges.subscription.field_type_placeholder": "Выберите тип значков", "badges.subscription.field_type_placeholder": "Выберите тип достижений",
"badges.subscription.no_types": "Нет доступных типов", "badges.subscription.no_types": "Нет доступных типов",
"badges.subscription.btn_create": "Добавить", "badges.subscription.btn_create": "Добавить",
"badges.subscription.btn_delete": "Удалить", "badges.subscription.btn_delete": "Удалить",
"badges.error.invalid_badge_id": "Не указан значок", "badges.error.invalid_badge_id": "Не указано достижение",
"badges.error.invalid_user_id": "Не указан пользователь", "badges.error.invalid_user_id": "Не указан пользователь",
"badges.error.no_permission_grant": "Недостаточно прав для выдачи этого значка", "badges.error.no_permission_grant": "Недостаточно прав для выдачи этого достижения",
"badges.error.cannot_grant_badge": "Не удалось выдать значок", "badges.error.cannot_grant_badge": "Не удалось выдать достижение",
"badges.error.user_not_found": "Пользователь не найден", "badges.error.user_not_found": "Пользователь не найден",
"badges.error.invalid_type_id": "Не указан тип значков", "badges.error.invalid_type_id": "Не указан тип достижений",
"badges.error.no_permission_subscription": "Недостаточно прав для управления подписками", "badges.error.no_permission_subscription": "Недостаточно прав для управления подписками",
"badges.error.cannot_create_subscription": "Не удалось создать подписку", "badges.error.cannot_create_subscription": "Не удалось создать подписку",
"badges.error.cannot_delete_subscription": "Не удалось удалить подписку", "badges.error.cannot_delete_subscription": "Не удалось удалить подписку",
"badges.error.ownership_not_found": "Выдача не найдена",
"badges.error.no_permission_revoke": "Недостаточно прав для снятия этого достижения",
"badges.error.cannot_revoke": "Не удалось снять достижение",
"badges.error.already_owned": "Это достижение уже выдано этому пользователю",
"badges.error.unknown": "Произошла ошибка", "badges.error.unknown": "Произошла ошибка",
"badges.error.cannot_get_user": "Не удалось получить данные пользователя", "badges.error.cannot_get_user": "Не удалось получить данные пользователя",
"badges.error.cannot_get_types": "Не удалось загрузить типы", "badges.error.cannot_get_types": "Не удалось загрузить типы",
"badges.error.cannot_get_badges": "Не удалось загрузить значки", "badges.error.cannot_get_badges": "Не удалось загрузить достижения",
"badges.error.invalid_request": "Неверный формат запроса", "badges.error.invalid_request": "Неверный формат запроса",
"badges.error.invalid_name": "Необходимо указать название", "badges.error.invalid_name": "Необходимо указать название",
"badges.error.invalid_image": "Необходимо указать эмодзи", "badges.error.invalid_image": "Необходимо указать эмодзи",
"badges.error.type_not_found": "Тип значка не найден", "badges.error.type_not_found": "Тип достижения не найден",
"badges.error.badge_not_found": "Значок не найден", "badges.error.badge_not_found": "Достижение не найдено",
"badges.error.no_permission": "Недостаточно прав для выполнения действия", "badges.error.no_permission": "Недостаточно прав для выполнения действия",
"badges.error.missing_badge_id": "Не указан ID значка", "badges.error.missing_badge_id": "Не указан ID достижения",
"badges.error.missing_type_id": "Не указан ID типа", "badges.error.missing_type_id": "Не указан ID типа",
"badges.error.cannot_create_badge": "Не удалось создать значок", "badges.error.cannot_create_badge": "Не удалось создать достижение",
"badges.error.cannot_create_type": "Не удалось создать тип", "badges.error.cannot_create_type": "Не удалось создать тип",
"badges.error.cannot_update_badge": "Не удалось обновить значок", "badges.error.cannot_update_badge": "Не удалось обновить достижение",
"badges.error.cannot_delete_badge": "Не удалось удалить значок", "badges.error.cannot_delete_badge": "Не удалось удалить достижение",
"badges.error.cannot_update_type": "Не удалось обновить тип", "badges.error.cannot_update_type": "Не удалось обновить тип",
"badges.error.cannot_delete_type": "Не удалось удалить тип", "badges.error.cannot_delete_type": "Не удалось удалить тип",

View File

@ -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, Badge, BadgeDetails, BadgeID, BadgeTypeDefinition, CreateBadgeRequest, CreateTypeRequest, GetTypesResponse, GrantBadgeRequest, SubscriptionRequest, UpdateBadgeRequest, UpdateTypeRequest, UserBadge} from 'types/badges'; import {AllBadgesBadge, Badge, BadgeDetails, BadgeID, BadgeTypeDefinition, CreateBadgeRequest, CreateTypeRequest, GetTypesResponse, GrantBadgeRequest, RevokeOwnershipRequest, SubscriptionRequest, UpdateBadgeRequest, UpdateTypeRequest, UserBadge} from 'types/badges';
export default class Client { export default class Client {
private url: string; private url: string;
@ -86,6 +86,10 @@ export default class Client {
await this.doPost(`${this.url}/deleteSubscription`, req); await this.doPost(`${this.url}/deleteSubscription`, req);
} }
async revokeOwnership(req: RevokeOwnershipRequest): Promise<void> {
await this.doPost(`${this.url}/revokeOwnership`, req);
}
async getChannelSubscriptions(channelID: string): Promise<BadgeTypeDefinition[]> { async getChannelSubscriptions(channelID: string): Promise<BadgeTypeDefinition[]> {
try { try {
const res = await this.doGet(`${this.url}/getChannelSubscriptions/${channelID}`); const res = await this.doGet(`${this.url}/getChannelSubscriptions/${channelID}`);

View File

@ -39,7 +39,7 @@ const BadgesAdminSetting: React.FC<Props> = ({id, value, disabled, onChange, set
<div className='help-text'> <div className='help-text'>
<FormattedMessage <FormattedMessage
id='badges.admin.help_text' id='badges.admin.help_text'
defaultMessage='Эти пользователи будут считаться администраторами плагина достижений. Они могут создавать типы, а также изменять и выдавать любые значки.' defaultMessage='Эти пользователи будут считаться администраторами плагина достижений. Они могут создавать типы, а также изменять и выдавать любые достижения.'
/> />
</div> </div>
</div> </div>

View File

@ -21,6 +21,7 @@ import InlineTypeForm from './inline_type_form';
import TypeSelect from './type_select'; import TypeSelect from './type_select';
import './badge_modal.scss'; import './badge_modal.scss';
import ConfirmDialog from 'components/confirm_dialog/confirm_dialog';
const NEW_TYPE_VALUE = '__new__'; const NEW_TYPE_VALUE = '__new__';
@ -193,7 +194,7 @@ const BadgeModal: React.FC = () => {
typeID = String(createdType.id); typeID = String(createdType.id);
} }
if (!typeID) { if (!typeID) {
setError(intl.formatMessage({id: 'badges.modal.error_type_required', defaultMessage: 'Выберите тип значка'})); setError(intl.formatMessage({id: 'badges.modal.error_type_required', defaultMessage: 'Выберите тип достижения'}));
setLoading(false); setLoading(false);
return; return;
} }
@ -252,8 +253,8 @@ const BadgeModal: React.FC = () => {
} }
const title = isEditMode const title = isEditMode
? intl.formatMessage({id: 'badges.modal.edit_badge_title', defaultMessage: 'Редактировать значок'}) ? intl.formatMessage({id: 'badges.modal.edit_badge_title', defaultMessage: 'Редактировать достижение'})
: intl.formatMessage({id: 'badges.modal.create_badge_title', defaultMessage: 'Создать значок'}); : intl.formatMessage({id: 'badges.modal.create_badge_title', defaultMessage: 'Создать достижение'});
const submitLabel = isEditMode const submitLabel = isEditMode
? intl.formatMessage({id: 'badges.modal.btn_save', defaultMessage: 'Сохранить'}) ? intl.formatMessage({id: 'badges.modal.btn_save', defaultMessage: 'Сохранить'})
: intl.formatMessage({id: 'badges.modal.btn_create', defaultMessage: 'Создать'}); : intl.formatMessage({id: 'badges.modal.btn_create', defaultMessage: 'Создать'});
@ -294,7 +295,7 @@ const BadgeModal: React.FC = () => {
value={form.name} value={form.name}
onChange={(e) => updateForm({name: e.target.value})} onChange={(e) => updateForm({name: e.target.value})}
maxLength={20} maxLength={20}
placeholder={intl.formatMessage({id: 'badges.modal.field_name_placeholder', defaultMessage: 'Название значка (макс. 20 символов)'})} placeholder={intl.formatMessage({id: 'badges.modal.field_name_placeholder', defaultMessage: 'Название достижения (макс. 20 символов)'})}
/> />
</div> </div>
<div className='form-group'> <div className='form-group'>
@ -308,7 +309,7 @@ const BadgeModal: React.FC = () => {
value={form.description} value={form.description}
onChange={(e) => updateForm({description: e.target.value})} onChange={(e) => updateForm({description: e.target.value})}
maxLength={120} maxLength={120}
placeholder={intl.formatMessage({id: 'badges.modal.field_description_placeholder', defaultMessage: 'Описание значка (макс. 120 символов)'})} placeholder={intl.formatMessage({id: 'badges.modal.field_description_placeholder', defaultMessage: 'Описание достижения (макс. 120 символов)'})}
/> />
</div> </div>
<div className='form-group'> <div className='form-group'>
@ -398,45 +399,27 @@ const BadgeModal: React.FC = () => {
{error && <div className='error-message'>{error}</div>} {error && <div className='error-message'>{error}</div>}
{isEditMode && ( {isEditMode && (
<div className='delete-section'> <div className='delete-section'>
{confirmDelete ? ( <button
<div className='confirm-delete'> className='btn btn--danger'
<span> onClick={handleDelete}
<FormattedMessage disabled={loading}
id='badges.modal.confirm_delete' >
defaultMessage='Вы уверены?' <FormattedMessage
/> id='badges.modal.btn_delete'
</span> defaultMessage='Удалить достижение'
<button />
className='btn btn--danger' </button>
onClick={handleDelete} {confirmDelete && (
disabled={loading} <ConfirmDialog
> onConfirm={handleDelete}
<FormattedMessage onCancel={() => setConfirmDelete(false)}
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 <FormattedMessage
id='badges.modal.btn_delete' id='badges.modal.confirm_delete_badge'
defaultMessage='Удалить значок' defaultMessage='Удалить достижение «{name}»?'
values={{name: form.name || editData?.name}}
/> />
</button> </ConfirmDialog>
)} )}
</div> </div>
)} )}

View File

@ -41,7 +41,7 @@ const InlineTypeForm: React.FC<Props> = ({form, onChange}) => {
<label htmlFor='newTypeEveryoneCanCreate'> <label htmlFor='newTypeEveryoneCanCreate'>
<FormattedMessage <FormattedMessage
id='badges.modal.new_type_everyone_create' id='badges.modal.new_type_everyone_create'
defaultMessage='Все могут создавать значки' defaultMessage='Все могут создавать достижения'
/> />
</label> </label>
</div> </div>
@ -69,7 +69,7 @@ const InlineTypeForm: React.FC<Props> = ({form, onChange}) => {
<label htmlFor='newTypeEveryoneCanGrant'> <label htmlFor='newTypeEveryoneCanGrant'>
<FormattedMessage <FormattedMessage
id='badges.modal.new_type_everyone_grant' id='badges.modal.new_type_everyone_grant'
defaultMessage='Все могут выдавать значки' defaultMessage='Все могут выдавать достижения'
/> />
</label> </label>
</div> </div>

View File

@ -36,7 +36,7 @@ const TypeSelect: React.FC<Props> = ({
const intl = useIntl(); const intl = useIntl();
const selectedTypeName = types.find((t) => String(t.id) === badgeType)?.name || const selectedTypeName = types.find((t) => String(t.id) === badgeType)?.name ||
Review

мб в мемо?

мб в мемо?
intl.formatMessage({id: 'badges.modal.field_type_placeholder', defaultMessage: 'Выберите тип значка'}); intl.formatMessage({id: 'badges.modal.field_type_placeholder', defaultMessage: 'Выберите тип достижения'});
const triggerLabel = showCreateType ? intl.formatMessage({id: 'badges.modal.create_new_type', defaultMessage: '+ Создать новый тип'}) : selectedTypeName; const triggerLabel = showCreateType ? intl.formatMessage({id: 'badges.modal.create_new_type', defaultMessage: '+ Создать новый тип'}) : selectedTypeName;
const confirmType = confirmDeleteTypeId ? types.find((t) => String(t.id) === confirmDeleteTypeId) : null; const confirmType = confirmDeleteTypeId ? types.find((t) => String(t.id) === confirmDeleteTypeId) : null;
Review

мб в мемо?

мб в мемо?

View File

@ -30,5 +30,31 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
.btn--cancel {
background: var(--center-channel-bg, #fff);
color: var(--center-channel-color, #3d3c40);
border: 1px solid rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.16);
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
&:hover {
background: rgba(var(--center-channel-color-rgb, 61, 60, 64), 0.08);
}
}
.btn--danger {
background: var(--error-text, #d24b4e);
color: #fff;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
&:hover {
background: color-mix(in srgb, var(--error-text, #d24b4e) 85%, #000);
}
}
} }
} }

View File

@ -11,7 +11,10 @@ type Props = {
} }
const ConfirmDialog: React.FC<Props> = ({children, onConfirm, onCancel}) => ( const ConfirmDialog: React.FC<Props> = ({children, onConfirm, onCancel}) => (
<div className='ConfirmDialog__overlay'> <div
className='ConfirmDialog__overlay'
onClick={(e) => e.stopPropagation()}
>
<div className='ConfirmDialog'> <div className='ConfirmDialog'>
<p className='ConfirmDialog__text'> <p className='ConfirmDialog__text'>
{children} {children}

View File

@ -45,7 +45,7 @@ const GrantModal: React.FC = () => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [closing, setClosing] = useState(false); const [closing, setClosing] = useState(false);
// Выбор значка // Выбор достижения
const [badgeDropdownOpen, setBadgeDropdownOpen] = useState(false); const [badgeDropdownOpen, setBadgeDropdownOpen] = useState(false);
const badgeDropdownRef = useRef<HTMLDivElement>(null); const badgeDropdownRef = useRef<HTMLDivElement>(null);
@ -71,7 +71,7 @@ const GrantModal: React.FC = () => {
}; };
fetchBadges(); fetchBadges();
// Prefill значка, если передан // Prefill достижения, если передан
if (modalData?.prefillBadgeId) { if (modalData?.prefillBadgeId) {
setForm((prev) => ({...prev, badgeId: modalData.prefillBadgeId || ''})); setForm((prev) => ({...prev, badgeId: modalData.prefillBadgeId || ''}));
} }
@ -153,7 +153,7 @@ const GrantModal: React.FC = () => {
<h4> <h4>
<FormattedMessage <FormattedMessage
id='badges.grant.title' id='badges.grant.title'
defaultMessage='Выдать значок' defaultMessage='Выдать достижение'
/> />
</h4> </h4>
<button <button
@ -168,7 +168,7 @@ const GrantModal: React.FC = () => {
<p className='grant-intro'> <p className='grant-intro'>
<FormattedMessage <FormattedMessage
id='badges.grant.intro' id='badges.grant.intro'
defaultMessage='Выдать значок пользователю @{username}' defaultMessage='Выдать достижение пользователю @{username}'
values={{username: modalData?.prefillUser || ''}} values={{username: modalData?.prefillUser || ''}}
/> />
</p> </p>
@ -177,7 +177,7 @@ const GrantModal: React.FC = () => {
<label> <label>
<FormattedMessage <FormattedMessage
id='badges.grant.field_badge' id='badges.grant.field_badge'
defaultMessage='Значок' defaultMessage='Достижение'
/> />
<span className='required'>{'*'}</span> <span className='required'>{'*'}</span>
</label> </label>
@ -199,7 +199,7 @@ const GrantModal: React.FC = () => {
/> />
{' '}{selectedBadge.name} {' '}{selectedBadge.name}
</> </>
) : intl.formatMessage({id: 'badges.grant.field_badge_placeholder', defaultMessage: 'Выберите значок'})} ) : intl.formatMessage({id: 'badges.grant.field_badge_placeholder', defaultMessage: 'Выберите достижение'})}
</span> </span>
<span className='type-select__arrow'>{'▾'}</span> <span className='type-select__arrow'>{'▾'}</span>
</button> </button>
@ -209,7 +209,7 @@ const GrantModal: React.FC = () => {
<div className='type-select__option'> <div className='type-select__option'>
<FormattedMessage <FormattedMessage
id='badges.grant.no_badges' id='badges.grant.no_badges'
defaultMessage='Нет доступных значков' defaultMessage='Нет доступных достижений'
/> />
</div> </div>
)} )}
@ -244,7 +244,7 @@ const GrantModal: React.FC = () => {
value={form.reason} value={form.reason}
onChange={(e) => updateForm({reason: e.target.value})} onChange={(e) => updateForm({reason: e.target.value})}
maxLength={200} maxLength={200}
placeholder={intl.formatMessage({id: 'badges.grant.field_reason_placeholder', defaultMessage: 'За что выдаётся значок? (необязательно)'})} placeholder={intl.formatMessage({id: 'badges.grant.field_reason_placeholder', defaultMessage: 'За что выдаётся достижение? (необязательно)'})}
/> />
</div> </div>
<div className='checkbox-group'> <div className='checkbox-group'>

View File

@ -112,13 +112,13 @@ const AllBadges: React.FC<Props> = ({filterTypeId, filterTypeName, actions}) =>
<div className='AllBadges__emptyTitle'> <div className='AllBadges__emptyTitle'>
<FormattedMessage <FormattedMessage
id='badges.empty.title' id='badges.empty.title'
defaultMessage='Значков пока нет' defaultMessage='Достижений пока нет'
/> />
</div> </div>
<div className='AllBadges__emptyDescription'> <div className='AllBadges__emptyDescription'>
<FormattedMessage <FormattedMessage
id='badges.empty.description' id='badges.empty.description'
defaultMessage='Создайте первый значок, чтобы отмечать достижения и заслуги участников команды.' defaultMessage='Создайте первое достижение, чтобы отмечать заслуги участников команды.'
/> />
</div> </div>
</div> </div>
@ -127,7 +127,7 @@ const AllBadges: React.FC<Props> = ({filterTypeId, filterTypeName, actions}) =>
<div className='AllBadges__empty'> <div className='AllBadges__empty'>
<FormattedMessage <FormattedMessage
id='badges.types.no_badges' id='badges.types.no_badges'
defaultMessage='В этом типе нет значков' defaultMessage='В этом типе нет достижений'
/> />
</div> </div>
)} )}

View File

@ -3,6 +3,7 @@ import React, {useState} from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {BadgeTypeDefinition} from '../../types/badges'; import {BadgeTypeDefinition} from '../../types/badges';
import ConfirmDialog from '../confirm_dialog/confirm_dialog';
import './all_types_row.scss'; import './all_types_row.scss';
@ -44,7 +45,7 @@ const AllTypesRow: React.FC<Props> = ({badgeType, onEdit, onDelete, onClick}: Pr
<div className='AllTypesRow__meta'> <div className='AllTypesRow__meta'>
<FormattedMessage <FormattedMessage
id='badges.types.badge_count' id='badges.types.badge_count'
defaultMessage='{count, plural, one {# значок} few {# значка} many {# значков} other {# значков}}' defaultMessage='{count, plural, one {# достижение} few {# достижения} many {# достижений} other {# достижений}}'
values={{count: badgeType.badge_count}} values={{count: badgeType.badge_count}}
/> />
{badgeType.can_create?.everyone && ( {badgeType.can_create?.everyone && (
@ -71,58 +72,37 @@ const AllTypesRow: React.FC<Props> = ({badgeType, onEdit, onDelete, onClick}: Pr
className='AllTypesRow__actions' className='AllTypesRow__actions'
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{!confirmDelete && ( <button
className='AllTypesRow__btn AllTypesRow__btn--edit'
onClick={() => onEdit(badgeType)}
>
<FormattedMessage
id='badges.rhs.edit_badge'
defaultMessage='Редактировать'
/>
</button>
{!badgeType.is_default && (
<button <button
className='AllTypesRow__btn AllTypesRow__btn--edit' className='AllTypesRow__btn AllTypesRow__btn--danger'
onClick={() => onEdit(badgeType)} onClick={handleDelete}
> >
<FormattedMessage <FormattedMessage
id='badges.rhs.edit_badge' id='badges.modal.delete_type'
defaultMessage='Редактировать' defaultMessage='Удалить'
/> />
</button> </button>
)} )}
{!badgeType.is_default && ( {confirmDelete && (
<> <ConfirmDialog
{confirmDelete ? ( onConfirm={() => onDelete(badgeType)}
<div className='AllTypesRow__confirmDelete'> onCancel={() => setConfirmDelete(false)}
<span className='AllTypesRow__confirmText'> >
<FormattedMessage <FormattedMessage
id='badges.modal.confirm_delete' id='badges.modal.confirm_delete_type'
defaultMessage='Вы уверены?' defaultMessage='Удалить тип «{name}»?'
/> values={{name: badgeType.name}}
</span> />
<button </ConfirmDialog>
className='AllTypesRow__btn AllTypesRow__btn--danger'
onClick={handleDelete}
>
<FormattedMessage
id='badges.modal.btn_confirm_delete_type'
defaultMessage='Да, удалить'
/>
</button>
<button
className='AllTypesRow__btn AllTypesRow__btn--cancel'
onClick={() => setConfirmDelete(false)}
>
<FormattedMessage
id='badges.modal.btn_cancel'
defaultMessage='Отмена'
/>
</button>
</div>
) : (
<button
className='AllTypesRow__btn AllTypesRow__btn--danger'
onClick={handleDelete}
>
<FormattedMessage
id='badges.modal.delete_type'
defaultMessage='Удалить'
/>
</button>
)}
</>
)} )}
</div> </div>
</div> </div>

View File

@ -93,7 +93,7 @@ class BadgeDetailsComponent extends React.PureComponent<Props, State> {
return (<div> return (<div>
<FormattedMessage <FormattedMessage
id='badges.badge_not_found' id='badges.badge_not_found'
defaultMessage='Значок не найден.' defaultMessage='Достижение не найдено.'
/> />
</div>); </div>);
} }
@ -108,7 +108,7 @@ class BadgeDetailsComponent extends React.PureComponent<Props, State> {
return (<div> return (<div>
<FormattedMessage <FormattedMessage
id='badges.badge_not_found' id='badges.badge_not_found'
defaultMessage='Значок не найден.' defaultMessage='Достижение не найдено.'
/> />
</div>); </div>);
} }

View File

@ -76,7 +76,7 @@ const RHS: React.FC = () => {
> >
<FormattedMessage <FormattedMessage
id='badges.rhs.all_badges' id='badges.rhs.all_badges'
defaultMessage='Все значки' defaultMessage='Все достижения'
/> />
</button> </button>
{canEditType && ( {canEditType && (
@ -98,7 +98,7 @@ const RHS: React.FC = () => {
> >
<FormattedMessage <FormattedMessage
id='badges.rhs.create_badge' id='badges.rhs.create_badge'
defaultMessage='+ Создать значок' defaultMessage='+ Создать достижение'
/> />
</button> </button>
)} )}
@ -163,6 +163,7 @@ const RHS: React.FC = () => {
<UserBadges <UserBadges
user={currentUser} user={currentUser}
isCurrentUser={false} isCurrentUser={false}
currentUserID={myUser.id}
actions={{ actions={{
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)),
@ -176,6 +177,7 @@ const RHS: React.FC = () => {
<UserBadges <UserBadges
user={myUser} user={myUser}
isCurrentUser={true} isCurrentUser={true}
currentUserID={myUser.id}
actions={{ actions={{
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)),

View File

@ -73,4 +73,38 @@
} }
} }
} }
.user-badge-revoke {
margin-top: 4px;
a {
font-size: 12px;
color: var(--error-text, #d24b4e);
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
&--confirm {
display: flex;
align-items: center;
gap: 8px;
}
&__text {
font-size: 12px;
color: var(--error-text, #d24b4e);
font-weight: 600;
}
&__yes {
font-weight: 600;
}
&__no {
color: rgba(var(--center-channel-color-rgb), 0.56) !important;
}
}
} }

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, {useState} from 'react';
import {FormattedMessage, useIntl} from 'react-intl'; import {FormattedMessage, useIntl} from 'react-intl';
@ -7,18 +7,42 @@ import Client4 from 'mattermost-redux/client/client4';
import {UserBadge} from '../../types/badges'; import {UserBadge} from '../../types/badges';
import BadgeImage from '../utils/badge_image'; import BadgeImage from '../utils/badge_image';
import {markdown} from 'utils/markdown'; import {markdown} from 'utils/markdown';
import Client from '../../client/api';
import ConfirmDialog from '../confirm_dialog/confirm_dialog';
import './user_badge_row.scss'; import './user_badge_row.scss';
type Props = { type Props = {
badge: UserBadge; badge: UserBadge;
isCurrentUser: boolean; isCurrentUser: boolean;
currentUserID: string;
onClick: (badge: UserBadge) => void; onClick: (badge: UserBadge) => void;
onRevoke?: (badge: UserBadge) => void;
} }
const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) => { const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser, currentUserID, onRevoke}: Props) => {
const intl = useIntl(); const intl = useIntl();
const time = new Date(badge.time); const time = new Date(badge.time);
const [confirmingRevoke, setConfirmingRevoke] = useState(false);
const canRevoke = badge.granted_by === currentUserID;
const handleRevoke = async () => {
try {
const client = new Client();
await client.revokeOwnership({
badge_id: String(badge.id),
user_id: badge.user,
time: String(badge.time),
});
onRevoke?.(badge);
} catch {
// ignore
} finally {
setConfirmingRevoke(false);
}
};
let reason = null; let reason = null;
if (badge.reason) { if (badge.reason) {
reason = ( reason = (
@ -50,6 +74,42 @@ const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) =
</div> </div>
); );
} }
let revokeAction = null;
if (canRevoke && onRevoke) {
revokeAction = (
<div className='user-badge-revoke'>
<a
onClick={(e) => {
e.stopPropagation();
setConfirmingRevoke(true);
}}
>
<FormattedMessage
id='badges.revoke.btn'
defaultMessage='Снять достижение'
/>
</a>
</div>
);
if (confirmingRevoke) {
revokeAction = (
<>
{revokeAction}
<ConfirmDialog
onConfirm={handleRevoke}
onCancel={() => setConfirmingRevoke(false)}
>
<FormattedMessage
id='badges.revoke.confirm'
defaultMessage='Снять достижение?'
/>
</ConfirmDialog>
</>
);
}
}
return ( return (
<div <div
className='UserBadgesRow' className='UserBadgesRow'
@ -105,6 +165,7 @@ const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) =
</div> </div>
{reason} {reason}
{setStatus} {setStatus}
{revokeAction}
</div> </div>
</div> </div>
); );

View File

@ -18,6 +18,7 @@ import './user_badges.scss';
type Props = { type Props = {
isCurrentUser: boolean; isCurrentUser: boolean;
currentUserID: string;
user: UserProfile | null; user: UserProfile | null;
actions: { actions: {
setRHSView: (view: RHSState) => void; setRHSView: (view: RHSState) => void;
@ -84,6 +85,17 @@ class UserBadges extends React.PureComponent<Props, State> {
this.props.actions.setRHSView(RHS_STATE_DETAIL); this.props.actions.setRHSView(RHS_STATE_DETAIL);
} }
onRevoke = () => {
if (!this.props.user) {
return;
}
const c = new Client();
this.setState({loading: true});
Review

пупупуууу

пупупуууу
c.getUserBadges(this.props.user.id).then((badges) => {
this.setState({badges, loading: false});
});
}
render() { render() {
if (!this.props.user) { if (!this.props.user) {
return (<div> return (<div>
@ -104,7 +116,7 @@ class UserBadges extends React.PureComponent<Props, State> {
return (<div> return (<div>
<FormattedMessage <FormattedMessage
id='badges.no_badges_yet' id='badges.no_badges_yet'
defaultMessage='Значков пока нет.' defaultMessage='Достижений пока нет.'
/> />
</div>); </div>);
} }
@ -113,9 +125,11 @@ class UserBadges extends React.PureComponent<Props, State> {
return ( return (
<UserBadgeRow <UserBadgeRow
isCurrentUser={this.props.isCurrentUser} isCurrentUser={this.props.isCurrentUser}
currentUserID={this.props.currentUserID}
key={badge.time} key={badge.time}
badge={badge} badge={badge}
onClick={this.onBadgeClick} onClick={this.onBadgeClick}
onRevoke={this.onRevoke}
/> />
); );
}); });
@ -123,12 +137,12 @@ class UserBadges extends React.PureComponent<Props, State> {
const title = this.props.isCurrentUser ? ( const title = this.props.isCurrentUser ? (
<FormattedMessage <FormattedMessage
id='badges.rhs.my_badges' id='badges.rhs.my_badges'
defaultMessage='Мои значки' defaultMessage='Мои достижения'
/> />
) : ( ) : (
<FormattedMessage <FormattedMessage
id='badges.rhs.user_badges' id='badges.rhs.user_badges'
defaultMessage='Значки @{username}' defaultMessage='Достижения @{username}'
values={{username: this.props.user.username}} values={{username: this.props.user.username}}
/> />
); );

View File

@ -128,7 +128,7 @@ const SubscriptionModal: React.FC = () => {
<label> <label>
<FormattedMessage <FormattedMessage
id='badges.subscription.field_type' id='badges.subscription.field_type'
defaultMessage='Тип значков' defaultMessage='Тип достижений'
/> />
<span className='required'>{'*'}</span> <span className='required'>{'*'}</span>
</label> </label>
@ -144,7 +144,7 @@ const SubscriptionModal: React.FC = () => {
<span className='type-select__value'> <span className='type-select__value'>
{selectedType {selectedType
? selectedType.name ? selectedType.name
: intl.formatMessage({id: 'badges.subscription.field_type_placeholder', defaultMessage: 'Выберите тип значков'}) : intl.formatMessage({id: 'badges.subscription.field_type_placeholder', defaultMessage: 'Выберите тип достижений'})
} }
</span> </span>
<span className='type-select__arrow'>{'▾'}</span> <span className='type-select__arrow'>{'▾'}</span>

View File

@ -10,6 +10,7 @@ import Client from 'client/api';
import {getServerErrorId} from 'utils/helpers'; import {getServerErrorId} from 'utils/helpers';
import CloseIcon from 'components/icons/close_icon'; import CloseIcon from 'components/icons/close_icon';
import UserMultiSelect from 'components/user_multi_select'; import UserMultiSelect from 'components/user_multi_select';
import ConfirmDialog from 'components/confirm_dialog/confirm_dialog';
const emptyTypeForm: TypeFormData = { const emptyTypeForm: TypeFormData = {
name: '', name: '',
@ -173,7 +174,7 @@ const TypeModal: React.FC = () => {
<label htmlFor='typeEveryoneCanCreate'> <label htmlFor='typeEveryoneCanCreate'>
<FormattedMessage <FormattedMessage
id='badges.modal.new_type_everyone_create' id='badges.modal.new_type_everyone_create'
defaultMessage='Все могут создавать значки' defaultMessage='Все могут создавать достижения'
/> />
</label> </label>
</div> </div>
@ -192,7 +193,7 @@ const TypeModal: React.FC = () => {
<span className='form-group__help'> <span className='form-group__help'>
<FormattedMessage <FormattedMessage
id='badges.modal.allowlist_create_help' id='badges.modal.allowlist_create_help'
defaultMessage='Пользователи, которые могут создавать значки этого типа.' defaultMessage='Пользователи, которые могут создавать достижения этого типа.'
/> />
</span> </span>
</div> </div>
@ -207,7 +208,7 @@ const TypeModal: React.FC = () => {
<label htmlFor='typeEveryoneCanGrant'> <label htmlFor='typeEveryoneCanGrant'>
<FormattedMessage <FormattedMessage
id='badges.modal.new_type_everyone_grant' id='badges.modal.new_type_everyone_grant'
defaultMessage='Все могут выдавать значки' defaultMessage='Все могут выдавать достижения'
/> />
</label> </label>
</div> </div>
@ -226,7 +227,7 @@ const TypeModal: React.FC = () => {
<span className='form-group__help'> <span className='form-group__help'>
<FormattedMessage <FormattedMessage
id='badges.modal.allowlist_grant_help' id='badges.modal.allowlist_grant_help'
defaultMessage='Пользователи, которые могут выдавать значки этого типа.' defaultMessage='Пользователи, которые могут выдавать достижения этого типа.'
/> />
</span> </span>
</div> </div>
@ -234,46 +235,27 @@ const TypeModal: React.FC = () => {
{error && <div className='error-message'>{error}</div>} {error && <div className='error-message'>{error}</div>}
{isEditMode && !editData?.is_default && ( {isEditMode && !editData?.is_default && (
<div className='delete-section'> <div className='delete-section'>
{confirmDelete ? ( <button
<div className='confirm-delete'> className='btn btn--danger'
<span> onClick={handleDelete}
<FormattedMessage disabled={loading}
id='badges.types.confirm_delete' >
defaultMessage='Удалить тип «{name}» и все его значки?' <FormattedMessage
values={{name: editData?.name}} id='badges.modal.btn_delete_type'
/> defaultMessage='Удалить тип'
</span> />
<button </button>
className='btn btn--danger' {confirmDelete && (
onClick={handleDelete} <ConfirmDialog
disabled={loading} onConfirm={handleDelete}
> onCancel={() => setConfirmDelete(false)}
<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 <FormattedMessage
id='badges.modal.btn_delete_type' id='badges.types.confirm_delete'
defaultMessage='Удалить тип' defaultMessage='Удалить тип «{name}» и все его достижения?'
values={{name: editData?.name}}
/> />
</button> </ConfirmDialog>
)} )}
</div> </div>
)} )}

View File

@ -42,7 +42,7 @@ type State = {
loaded?: boolean; loaded?: boolean;
} }
Review

client

client
const MAX_BADGES = 7; const MAX_BADGES = 6;
const BADGE_SIZE = 24; const BADGE_SIZE = 24;
class BadgeList extends React.PureComponent<Props, State> { class BadgeList extends React.PureComponent<Props, State> {
@ -229,7 +229,7 @@ class BadgeList extends React.PureComponent<Props, State> {
<div><b> <div><b>
<FormattedMessage <FormattedMessage
id='badges.popover.title' id='badges.popover.title'
defaultMessage='Значки' defaultMessage='Достижения'
/> />
</b></div> </b></div>
<div id='contentContainer' > <div id='contentContainer' >
@ -244,7 +244,7 @@ class BadgeList extends React.PureComponent<Props, State> {
<span className={'fa fa-plus-circle'}/> <span className={'fa fa-plus-circle'}/>
<FormattedMessage <FormattedMessage
id='badges.grant_badge' id='badges.grant_badge'
defaultMessage='Выдать значок' defaultMessage='Выдать достижение'
/> />
</button> </button>
<hr className='divider divider--expanded'/> <hr className='divider divider--expanded'/>

View File

@ -128,3 +128,9 @@ export type SubscriptionRequest = {
type_id: string; type_id: string;
channel_id: string; channel_id: string;
} }
export type RevokeOwnershipRequest = {
badge_id: string;
user_id: string;
time: string;
}