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 (
"encoding/json"
"errors"
"net/http"
"runtime/debug"
"strings"
@ -81,6 +82,12 @@ type SubscriptionAPIRequest struct {
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 {
*badgesmodel.BadgeTypeDefinition
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("/deleteType/{typeID}", p.extractUserMiddleWare(p.apiDeleteType, ResponseTypeJSON)).Methods(http.MethodDelete)
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("/createSubscription", p.extractUserMiddleWare(p.apiCreateSubscription, 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)
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 {
p.writeAPIError(w, &APIErrorResponse{
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)
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 {
p.writeAPIError(w, &APIErrorResponse{
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)
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 {
p.writeAPIError(w, &APIErrorResponse{
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)
_, _ = 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, "")
if err == errAlreadyOwned {
return commandError(T("badges.error.already_owned", "Это достижение уже выдано этому пользователю"))
}
if err != nil {
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.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.create_type.title", "translation": "Create type"},
{"id": "badges.dialog.create_type.submit", "translation": "Create"},
{"id": "badges.dialog.edit_type.title", "translation": "Edit type"},
{"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.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.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.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.description", "translation": "Description"},
@ -23,45 +23,46 @@
{"id": "badges.field.image.help", "translation": "Enter an emoticon name"},
{"id": "badges.field.type", "translation": "Type"},
{"id": "badges.field.multiple", "translation": "Multiple"},
{"id": "badges.field.multiple.help", "translation": "Whether the badge can be granted multiple times"},
{"id": "badges.field.delete_badge", "translation": "Delete badge"},
{"id": "badges.field.delete_badge.help", "translation": "WARNING: checking this will remove this badge permanently."},
{"id": "badges.field.everyone_can_create", "translation": "Everyone can create badge"},
{"id": "badges.field.everyone_can_create.help", "translation": "Whether any user can create a badge of this type"},
{"id": "badges.field.multiple.help", "translation": "Whether the achievement can be granted multiple times"},
{"id": "badges.field.delete_badge", "translation": "Delete achievement"},
{"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 achievement"},
{"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.help", "translation": "Fill the usernames separated by comma (,) of the people that can create badges of this type."},
{"id": "badges.field.everyone_can_grant", "translation": "Everyone can grant badge"},
{"id": "badges.field.everyone_can_grant.help", "translation": "Whether any user can grant a badge 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 achievement"},
{"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.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.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.badge", "translation": "Badge"},
{"id": "badges.field.badge", "translation": "Achievement"},
{"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.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.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.create_badge_or_type", "translation": "You can create either badge or type"},
{"id": "badges.error.no_types_available", "translation": "You cannot create badges from any type."},
{"id": "badges.error.must_set_badge_id", "translation": "You must set the badge ID"},
{"id": "badges.error.cannot_edit_badge", "translation": "You cannot edit this badge"},
{"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 achievements from any type."},
{"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 achievement"},
{"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.no_permissions_edit_type", "translation": "You have no permissions to edit a badge 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 an achievement type."},
{"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.no_permissions_grant", "translation": "You have no permissions to grant this badge"},
{"id": "badges.error.cannot_grant_badge", "translation": "You cannot grant that 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 achievement"},
{"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.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.granted", "translation": "Granted"},
@ -72,8 +73,8 @@
{"id": "badges.api.empty_emoji", "translation": "Empty emoji"},
{"id": "badges.api.invalid_field", "translation": "Invalid field"},
{"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.badge_created", "translation": "Badge `%s` created."},
{"id": "badges.api.no_permissions_create_badge", "translation": "You have no permissions to create this achievement"},
{"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.cannot_find_user", "translation": "Cannot find user"},
{"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.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.cannot_get_badge", "translation": "Cannot get badge"},
{"id": "badges.api.cannot_edit_badge", "translation": "You cannot edit this badge"},
{"id": "badges.api.could_not_get_badge", "translation": "Could not get the badge"},
{"id": "badges.api.no_permissions_edit_badge", "translation": "You have no permissions to edit this badge"},
{"id": "badges.api.badge_updated", "translation": "Badge `%s` updated."},
{"id": "badges.api.badge_not_found", "translation": "Badge not found"},
{"id": "badges.api.no_permissions_grant", "translation": "You have no permissions to grant this badge"},
{"id": "badges.api.cannot_get_badge", "translation": "Cannot get achievement"},
{"id": "badges.api.cannot_edit_badge", "translation": "You cannot edit this achievement"},
{"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 achievement"},
{"id": "badges.api.badge_updated", "translation": "Achievement `%s` updated."},
{"id": "badges.api.badge_not_found", "translation": "Achievement not found"},
{"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.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.subscription_added", "translation": "Subscription added"},
{"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.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.title", "translation": "%sbadge granted!"},
{"id": "badges.notify.channel_text", "translation": "@%s granted @%s the %s`%s` badge."},
{"id": "badges.notify.title", "translation": "%sachievement granted!"},
{"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."}
]

View File

@ -18,6 +18,7 @@ type Bundle i18n.Bundle
func Init() *Bundle {
bundle := i18n.NewBundle(language.Russian)
_, _ = bundle.LoadMessageFileFS(i18nFiles, "en.json")
_, _ = bundle.LoadMessageFileFS(i18nFiles, "ru.json")
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.edit_badge.title", "translation": "Редактировать значок"},
{"id": "badges.dialog.edit_badge.title", "translation": "Редактировать достижение"},
{"id": "badges.dialog.edit_badge.submit", "translation": "Сохранить"},
{"id": "badges.dialog.create_type.title", "translation": "Создать тип"},
{"id": "badges.dialog.create_type.submit", "translation": "Создать"},
{"id": "badges.dialog.edit_type.title", "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.intro", "translation": "Выдать значок пользователю @%s"},
{"id": "badges.dialog.grant.intro", "translation": "Выдать достижение пользователю @%s"},
{"id": "badges.dialog.create_subscription.title", "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.submit", "translation": "Удалить"},
{"id": "badges.dialog.delete_subscription.intro", "translation": "Выберите тип значка, подписку на который хотите удалить из этого канала."},
{"id": "badges.dialog.delete_subscription.intro", "translation": "Выберите тип достижения, подписку на который хотите удалить из этого канала."},
{"id": "badges.field.name", "translation": "Название"},
{"id": "badges.field.description", "translation": "Описание"},
@ -23,45 +23,46 @@
{"id": "badges.field.image.help", "translation": "Введите название эмодзи"},
{"id": "badges.field.type", "translation": "Тип"},
{"id": "badges.field.multiple", "translation": "Многократный"},
{"id": "badges.field.multiple.help", "translation": "Можно ли выдавать этот значок несколько раз"},
{"id": "badges.field.delete_badge", "translation": "Удалить значок"},
{"id": "badges.field.delete_badge.help", "translation": "ВНИМАНИЕ: если отметить, значок будет удалён безвозвратно."},
{"id": "badges.field.everyone_can_create", "translation": "Все могут создавать значки"},
{"id": "badges.field.everyone_can_create.help", "translation": "Любой пользователь может создать значок этого типа"},
{"id": "badges.field.multiple.help", "translation": "Можно ли выдавать это достижение несколько раз"},
{"id": "badges.field.delete_badge", "translation": "Удалить достижение"},
{"id": "badges.field.delete_badge.help", "translation": "ВНИМАНИЕ: если отметить, достижение будет удалён безвозвратно."},
{"id": "badges.field.everyone_can_create", "translation": "Все могут создавать достижения"},
{"id": "badges.field.everyone_can_create.help", "translation": "Любой пользователь может создать достижение этого типа"},
{"id": "badges.field.allowlist_create", "translation": "Список допущенных к созданию"},
{"id": "badges.field.allowlist_create.help", "translation": "Укажите имена пользователей через запятую (,), которые могут создавать значки этого типа."},
{"id": "badges.field.everyone_can_grant", "translation": "Все могут выдавать значки"},
{"id": "badges.field.everyone_can_grant.help", "translation": "Любой пользователь может выдать значок этого типа"},
{"id": "badges.field.allowlist_create.help", "translation": "Укажите имена пользователей через запятую (,), которые могут создавать достижения этого типа."},
{"id": "badges.field.everyone_can_grant", "translation": "Все могут выдавать достижения"},
{"id": "badges.field.everyone_can_grant.help", "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.help", "translation": "ВНИМАНИЕ: если отметить, этот тип и все связанные значки будут удалены безвозвратно."},
{"id": "badges.field.delete_type.help", "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.help", "translation": "Причина выдачи значка. Будет видна пользователю и в уведомлениях о выдаче (например, в подписках)."},
{"id": "badges.field.reason.help", "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.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.create_badge_or_type", "translation": "Можно создать badge или type"},
{"id": "badges.error.no_types_available", "translation": "Вы не можете создать значки ни одного типа."},
{"id": "badges.error.must_set_badge_id", "translation": "Необходимо указать ID значка"},
{"id": "badges.error.cannot_edit_badge", "translation": "У вас нет прав на редактирование этого значка"},
{"id": "badges.error.no_types_available", "translation": "Вы не можете создать достижения ни одного типа."},
{"id": "badges.error.must_set_badge_id", "translation": "Необходимо указать ID достижения"},
{"id": "badges.error.cannot_edit_badge", "translation": "У вас нет прав на редактирование этого достижения"},
{"id": "badges.error.specify_edit", "translation": "Укажите, что вы хотите отредактировать."},
{"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.cannot_edit_type", "translation": "У вас нет прав на редактирование этого типа"},
{"id": "badges.error.no_permissions_grant", "translation": "У вас нет прав на выдачу этого значка"},
{"id": "badges.error.cannot_grant_badge", "translation": "Вы не можете выдать этот значок"},
{"id": "badges.error.no_permissions_grant", "translation": "У вас нет прав на выдачу этого достижения"},
{"id": "badges.error.cannot_grant_badge", "translation": "Вы не можете выдать это достижение"},
{"id": "badges.error.specify_subscription", "translation": "Укажите, что вы хотите сделать."},
{"id": "badges.error.create_or_delete_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.granted", "translation": "Выдано"},
@ -72,8 +73,8 @@
{"id": "badges.api.empty_emoji", "translation": "Пустой эмодзи"},
{"id": "badges.api.invalid_field", "translation": "Некорректное поле"},
{"id": "badges.api.type_not_exist", "translation": "Этот тип не существует"},
{"id": "badges.api.no_permissions_create_badge", "translation": "У вас нет прав на создание этого значка"},
{"id": "badges.api.badge_created", "translation": "Значок `%s` создан."},
{"id": "badges.api.no_permissions_create_badge", "translation": "У вас нет прав на создание этого достижения"},
{"id": "badges.api.badge_created", "translation": "Достижение `%s` создано."},
{"id": "badges.api.no_permissions_create_type", "translation": "У вас нет прав на создание типа"},
{"id": "badges.api.cannot_find_user", "translation": "Не удалось найти пользователя"},
{"id": "badges.api.error_getting_user", "translation": "Ошибка получения пользователя %s: %v"},
@ -83,15 +84,15 @@
{"id": "badges.api.could_not_get_type", "translation": "Не удалось получить тип"},
{"id": "badges.api.no_permissions_edit_type", "translation": "У вас нет прав на редактирование этого типа"},
{"id": "badges.api.type_updated", "translation": "Тип `%s` обновлён."},
{"id": "badges.api.cannot_get_badge", "translation": "Не удалось получить значок"},
{"id": "badges.api.cannot_edit_badge", "translation": "Вы не можете редактировать этот значок"},
{"id": "badges.api.could_not_get_badge", "translation": "Не удалось получить значок"},
{"id": "badges.api.no_permissions_edit_badge", "translation": "У вас нет прав на редактирование этого значка"},
{"id": "badges.api.badge_updated", "translation": "Значок `%s` обновлён."},
{"id": "badges.api.badge_not_found", "translation": "Значок не найден"},
{"id": "badges.api.no_permissions_grant", "translation": "У вас нет прав на выдачу этого значка"},
{"id": "badges.api.cannot_get_badge", "translation": "Не удалось получить достижение"},
{"id": "badges.api.cannot_edit_badge", "translation": "Вы не можете редактировать это достижение"},
{"id": "badges.api.could_not_get_badge", "translation": "Не удалось получить достижение"},
{"id": "badges.api.no_permissions_edit_badge", "translation": "У вас нет прав на редактирование этого достижения"},
{"id": "badges.api.badge_updated", "translation": "Достижение `%s` обновлёно."},
{"id": "badges.api.badge_not_found", "translation": "Достижение не найдено"},
{"id": "badges.api.no_permissions_grant", "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.subscription_added", "translation": "Подписка добавлена"},
{"id": "badges.api.cannot_delete_subscription", "translation": "Вы не можете удалить подписку"},
@ -99,9 +100,9 @@
{"id": "badges.api.cannot_delete_default_type", "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.title", "translation": "%sзначок выдан!"},
{"id": "badges.notify.channel_text", "translation": "@%s выдал @%s значок %s`%s`."},
{"id": "badges.notify.title", "translation": "%sдостижение выдано!"},
{"id": "badges.notify.channel_text", "translation": "@%s выдал @%s достижение %s`%s`."},
{"id": "badges.notify.no_permission_channel", "translation": "У вас нет прав на отправку уведомления о выдаче в этот канал."}
]

View File

@ -12,6 +12,7 @@ import (
var errInvalidBadge = errors.New("invalid badge")
var errBadgeNotFound = errors.New("badge not found")
var errAlreadyOwned = errors.New("already owned")
type Store interface {
// Interface
@ -33,6 +34,8 @@ type Store interface {
UpdateBadge(b *badgesmodel.Badge) error
DeleteType(tID badgesmodel.BadgeType) 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
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) })
}
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 {
toRemove := badgesmodel.Subscription{ChannelID: cID, TypeID: tID}
return s.doAtomic(func() (bool, error) { return s.atomicRemoveSubscription(toRemove) })

View File

@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"errors"
"time"
"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) {
return false, true, nil
return false, true, errAlreadyOwned
}
ownership = append(ownership, o)
@ -159,6 +160,28 @@ func (s *store) atomicUpdateBadge(b *badgesmodel.Badge) (bool, error) {
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) {
subs, data, err := s.getAllSubscriptions()
if err != nil {

View File

@ -1,16 +1,16 @@
{
"badges.loading": "Loading...",
"badges.no_badges_yet": "No badges yet.",
"badges.empty.title": "No badges yet",
"badges.empty.description": "Create your first badge to recognize achievements and contributions of your team members.",
"badges.badge_not_found": "Badge not found.",
"badges.no_badges_yet": "No achievements yet.",
"badges.empty.title": "No achievements yet",
"badges.empty.description": "Create your first achievement to recognize contributions of your team members.",
"badges.badge_not_found": "Achievement not found.",
"badges.user_not_found": "User not found.",
"badges.unknown": "unknown",
"badges.rhs.all_badges": "All badges",
"badges.rhs.my_badges": "My badges",
"badges.rhs.user_badges": "@{username}'s badges",
"badges.rhs.badge_details": "Badge Details",
"badges.rhs.all_badges": "All achievements",
"badges.rhs.my_badges": "My achievements",
"badges.rhs.user_badges": "@{username}'s achievements",
"badges.rhs.badge_details": "Achievement Details",
"badges.label.name": "Name:",
"badges.label.description": "Description:",
@ -27,55 +27,56 @@
"badges.granted_to": "Granted to:",
"badges.not_granted_yet": "Not granted to anyone yet",
"badges.set_status": "Set status to this badge",
"badges.grant_badge": "Grant badge",
"badges.set_status": "Set status to this achievement",
"badges.grant_badge": "Grant achievement",
"badges.and_more": "and {count} more. Click to see all.",
"badges.menu.open_list": "Open the list of all badges.",
"badges.menu.create_badge": "Create badge",
"badges.menu.create_type": "Create badge type",
"badges.menu.add_subscription": "Add badge subscription",
"badges.menu.remove_subscription": "Remove badge subscription",
"badges.menu.open_list": "Open the list of all achievements.",
"badges.menu.create_badge": "Create achievement",
"badges.menu.create_type": "Create achievement type",
"badges.menu.add_subscription": "Add achievement subscription",
"badges.menu.remove_subscription": "Remove achievement subscription",
"badges.sidebar.title": "Badges",
"badges.popover.title": "Badges",
"badges.sidebar.title": "Achievements",
"badges.popover.title": "Achievements",
"badges.admin.label": "Achievements Administrators:",
"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.rhs.create_badge": "+ Create badge",
"badges.rhs.create_badge": "+ Create achievement",
"badges.rhs.edit_badge": "Edit",
"badges.rhs.types": "Types",
"badges.rhs.create_type": "+ Create type",
"badges.modal.create_badge_title": "Create Badge",
"badges.modal.edit_badge_title": "Edit Badge",
"badges.modal.create_badge_title": "Create Achievement",
"badges.modal.edit_badge_title": "Edit Achievement",
"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_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_placeholder": "Emoji name (e.g. star)",
"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.create_new_type": "+ Create new type",
"badges.modal.new_type_name": "Type name",
"badges.modal.new_type_name_placeholder": "Type name (max 20 chars)",
"badges.modal.new_type_everyone_create": "Everyone can create badges",
"badges.modal.new_type_everyone_grant": "Everyone can grant badges",
"badges.modal.new_type_everyone_create": "Everyone can create achievements",
"badges.modal.new_type_everyone_grant": "Everyone can grant achievements",
"badges.modal.btn_cancel": "Cancel",
"badges.modal.btn_create": "Create",
"badges.modal.btn_save": "Save",
"badges.modal.btn_creating": "Saving...",
"badges.modal.btn_delete": "Delete badge",
"badges.modal.btn_delete": "Delete achievement",
"badges.modal.btn_confirm_delete": "Yes, delete",
"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_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.edit_type_title": "Edit Type",
"badges.modal.btn_delete_type": "Delete type",
@ -83,65 +84,73 @@
"badges.modal.confirm_delete_type": "Delete type \"{name}\"?",
"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_grant": "Everyone grants",
"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.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.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_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.grant.title": "Grant Badge",
"badges.grant.intro": "Grant badge to @{username}",
"badges.grant.field_badge": "Badge",
"badges.grant.field_badge_placeholder": "Select a badge",
"badges.grant.no_badges": "No badges available",
"badges.grant.title": "Grant Achievement",
"badges.grant.intro": "Grant achievement to @{username}",
"badges.grant.field_badge": "Achievement",
"badges.grant.field_badge_placeholder": "Select an achievement",
"badges.grant.no_badges": "No achievements available",
"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.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_delete": "Remove Subscription",
"badges.subscription.field_type": "Badge Type",
"badges.subscription.field_type_placeholder": "Select badge type",
"badges.subscription.field_type": "Achievement Type",
"badges.subscription.field_type_placeholder": "Select achievement type",
"badges.subscription.no_types": "No types available",
"badges.subscription.btn_create": "Add",
"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.no_permission_grant": "Insufficient permissions to grant this badge",
"badges.error.cannot_grant_badge": "Failed to grant badge",
"badges.error.no_permission_grant": "Insufficient permissions to grant this achievement",
"badges.error.cannot_grant_badge": "Failed to grant achievement",
"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.cannot_create_subscription": "Failed to create 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.cannot_get_user": "Failed to get user data",
"badges.error.cannot_get_types": "Failed to load types",
"badges.error.cannot_get_badges": "Failed to load badges",
"badges.error.cannot_get_badges": "Failed to load achievements",
"badges.error.invalid_request": "Invalid request format",
"badges.error.invalid_name": "Name is required",
"badges.error.invalid_image": "Emoji is required",
"badges.error.type_not_found": "Badge type not found",
"badges.error.badge_not_found": "Badge not found",
"badges.error.type_not_found": "Achievement type not found",
"badges.error.badge_not_found": "Achievement not found",
"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.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_update_badge": "Failed to update badge",
"badges.error.cannot_delete_badge": "Failed to delete badge",
"badges.error.cannot_update_badge": "Failed to update achievement",
"badges.error.cannot_delete_badge": "Failed to delete achievement",
"badges.error.cannot_update_type": "Failed to update type",
"badges.error.cannot_delete_type": "Failed to delete type",

View File

@ -1,16 +1,16 @@
{
"badges.loading": "Загрузка...",
"badges.no_badges_yet": "Значков пока нет.",
"badges.empty.title": "Значков пока нет",
"badges.empty.description": "Создайте первый значок, чтобы отмечать достижения и заслуги участников команды.",
"badges.badge_not_found": "Значок не найден.",
"badges.no_badges_yet": "Достижений пока нет.",
"badges.empty.title": "Достижений пока нет",
"badges.empty.description": "Создайте первое достижение, чтобы отмечать заслуги участников команды.",
"badges.badge_not_found": "Достижение не найдено.",
"badges.user_not_found": "Пользователь не найден.",
"badges.unknown": "неизвестно",
"badges.rhs.all_badges": "Все значки",
"badges.rhs.my_badges": "Мои значки",
"badges.rhs.user_badges": "Значки @{username}",
"badges.rhs.badge_details": "Детали значка",
"badges.rhs.all_badges": "Все достижения",
"badges.rhs.my_badges": "Мои достижения",
"badges.rhs.user_badges": "Достижения @{username}",
"badges.rhs.badge_details": "Детали достижения",
"badges.label.name": "Название:",
"badges.label.description": "Описание:",
@ -28,54 +28,55 @@
"badges.not_granted_yet": "Ещё никому не выдан",
"badges.set_status": "Установить как статус",
"badges.grant_badge": "Выдать значок",
"badges.grant_badge": "Выдать достижение",
"badges.and_more": "и ещё {count}. Нажмите, чтобы увидеть все.",
"badges.menu.open_list": "Открыть список всех значков.",
"badges.menu.create_badge": "Создать значок",
"badges.menu.create_type": "Создать тип значков",
"badges.menu.add_subscription": "Добавить подписку на значки",
"badges.menu.remove_subscription": "Удалить подписку на значки",
"badges.menu.open_list": "Открыть список всех достижений.",
"badges.menu.create_badge": "Создать достижение",
"badges.menu.create_type": "Создать тип достижений",
"badges.menu.add_subscription": "Добавить подписку на достижения",
"badges.menu.remove_subscription": "Удалить подписку на достижения",
"badges.sidebar.title": "Значки",
"badges.popover.title": "Значки",
"badges.sidebar.title": "Достижения",
"badges.popover.title": "Достижения",
"badges.admin.label": "Администраторы достижений:",
"badges.admin.placeholder": "Начните вводить имя...",
"badges.admin.help_text": "Эти пользователи будут считаться администраторами плагина достижений. Они могут создавать типы, а также изменять и выдавать любые значки.",
"badges.admin.help_text": "Эти пользователи будут считаться администраторами плагина достижений. Они могут создавать типы, а также изменять и выдавать любые достижения.",
"badges.admin.no_results": "Пользователь не найден",
"badges.rhs.create_badge": "+ Создать значок",
"badges.rhs.create_badge": "+ Создать достижение",
"badges.rhs.edit_badge": "Редактировать",
"badges.rhs.types": "Типы",
"badges.rhs.create_type": "+ Создать тип",
"badges.modal.create_badge_title": "Создать значок",
"badges.modal.edit_badge_title": "Редактировать значок",
"badges.modal.create_badge_title": "Создать достижение",
"badges.modal.edit_badge_title": "Редактировать достижение",
"badges.modal.field_name": "Название",
"badges.modal.field_name_placeholder": "Название значка (макс. 20 символов)",
"badges.modal.field_name_placeholder": "Название достижения (макс. 20 символов)",
"badges.modal.field_description": "Описание",
"badges.modal.field_description_placeholder": "Описание значка (макс. 120 символов)",
"badges.modal.field_description_placeholder": "Описание достижения (макс. 120 символов)",
"badges.modal.field_image": "Эмодзи",
"badges.modal.field_image_placeholder": "Название эмодзи (напр. star)",
"badges.modal.field_type": "Тип",
"badges.modal.field_type_placeholder": "Выберите тип значка",
"badges.modal.field_type_placeholder": "Выберите тип достижения",
"badges.modal.field_multiple": "Можно выдавать несколько раз",
"badges.modal.create_new_type": "+ Создать новый тип",
"badges.modal.new_type_name": "Название типа",
"badges.modal.new_type_name_placeholder": "Название типа (макс. 20 символов)",
"badges.modal.new_type_everyone_create": "Все могут создавать значки",
"badges.modal.new_type_everyone_grant": "Все могут выдавать значки",
"badges.modal.new_type_everyone_create": "Все могут создавать достижения",
"badges.modal.new_type_everyone_grant": "Все могут выдавать достижения",
"badges.modal.btn_cancel": "Отмена",
"badges.modal.btn_create": "Создать",
"badges.modal.btn_save": "Сохранить",
"badges.modal.btn_creating": "Сохранение...",
"badges.modal.btn_delete": "Удалить значок",
"badges.modal.btn_delete": "Удалить достижение",
"badges.modal.btn_confirm_delete": "Да, удалить",
"badges.modal.confirm_delete": "Вы уверены?",
"badges.modal.confirm_delete_badge": "Удалить достижение «{name}»?",
"badges.modal.error_generic": "Произошла ошибка",
"badges.modal.error_type_name_required": "Введите название типа",
"badges.modal.error_type_required": "Выберите тип значка",
"badges.modal.error_type_required": "Выберите тип достижения",
"badges.modal.create_type_title": "Создать тип",
"badges.modal.edit_type_title": "Редактировать тип",
"badges.modal.btn_delete_type": "Удалить тип",
@ -83,65 +84,73 @@
"badges.modal.confirm_delete_type": "Удалить тип «{name}»?",
"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_grant": "Все выдают",
"badges.types.is_default": "По умолчанию",
"badges.types.confirm_delete": "Удалить тип «{name}» и все его значки?",
"badges.types.confirm_delete": "Удалить тип «{name}» и все его достижения?",
"badges.types.empty": "Типов пока нет",
"badges.types.no_badges": "В этом типе нет значков",
"badges.types.no_badges": "В этом типе нет достижений",
"badges.rhs.back_to_types": "Назад к типам",
"badges.modal.allowlist_create": "Список допущенных к созданию",
"badges.modal.allowlist_create_help": "Пользователи, которые могут создавать значки этого типа.",
"badges.modal.allowlist_create_help": "Пользователи, которые могут создавать достижения этого типа.",
"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.grant.title": "Выдать значок",
"badges.grant.intro": "Выдать значок пользователю @{username}",
"badges.grant.field_badge": "Значок",
"badges.grant.field_badge_placeholder": "Выберите значок",
"badges.grant.no_badges": "Нет доступных значков",
"badges.grant.title": "Выдать достижение",
"badges.grant.intro": "Выдать достижение пользователю @{username}",
"badges.grant.field_badge": "Достижение",
"badges.grant.field_badge_placeholder": "Выберите достижение",
"badges.grant.no_badges": "Нет доступных достижений",
"badges.grant.field_reason": "Причина",
"badges.grant.field_reason_placeholder": "За что выдаётся значок? (необязательно)",
"badges.grant.field_reason_placeholder": "За что выдаётся достижение? (необязательно)",
"badges.grant.notify_here": "Уведомить в канале",
"badges.grant.btn_grant": "Выдать",
"badges.revoke.btn": "Снять достижение",
"badges.revoke.confirm": "Снять достижение?",
"badges.revoke.confirm_yes": "Да",
"badges.subscription.title_create": "Добавить подписку",
"badges.subscription.title_delete": "Удалить подписку",
"badges.subscription.field_type": "Тип значков",
"badges.subscription.field_type_placeholder": "Выберите тип значков",
"badges.subscription.field_type": "Тип достижений",
"badges.subscription.field_type_placeholder": "Выберите тип достижений",
"badges.subscription.no_types": "Нет доступных типов",
"badges.subscription.btn_create": "Добавить",
"badges.subscription.btn_delete": "Удалить",
"badges.error.invalid_badge_id": "Не указан значок",
"badges.error.invalid_badge_id": "Не указано достижение",
"badges.error.invalid_user_id": "Не указан пользователь",
"badges.error.no_permission_grant": "Недостаточно прав для выдачи этого значка",
"badges.error.cannot_grant_badge": "Не удалось выдать значок",
"badges.error.no_permission_grant": "Недостаточно прав для выдачи этого достижения",
"badges.error.cannot_grant_badge": "Не удалось выдать достижение",
"badges.error.user_not_found": "Пользователь не найден",
"badges.error.invalid_type_id": "Не указан тип значков",
"badges.error.invalid_type_id": "Не указан тип достижений",
"badges.error.no_permission_subscription": "Недостаточно прав для управления подписками",
"badges.error.cannot_create_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.cannot_get_user": "Не удалось получить данные пользователя",
"badges.error.cannot_get_types": "Не удалось загрузить типы",
"badges.error.cannot_get_badges": "Не удалось загрузить значки",
"badges.error.cannot_get_badges": "Не удалось загрузить достижения",
"badges.error.invalid_request": "Неверный формат запроса",
"badges.error.invalid_name": "Необходимо указать название",
"badges.error.invalid_image": "Необходимо указать эмодзи",
"badges.error.type_not_found": "Тип значка не найден",
"badges.error.badge_not_found": "Значок не найден",
"badges.error.type_not_found": "Тип достижения не найден",
"badges.error.badge_not_found": "Достижение не найдено",
"badges.error.no_permission": "Недостаточно прав для выполнения действия",
"badges.error.missing_badge_id": "Не указан ID значка",
"badges.error.missing_badge_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_update_badge": "Не удалось обновить значок",
"badges.error.cannot_delete_badge": "Не удалось удалить значок",
"badges.error.cannot_update_badge": "Не удалось обновить достижение",
"badges.error.cannot_delete_badge": "Не удалось удалить достижение",
"badges.error.cannot_update_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 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 {
private url: string;
@ -86,6 +86,10 @@ export default class Client {
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[]> {
try {
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'>
<FormattedMessage
id='badges.admin.help_text'
defaultMessage='Эти пользователи будут считаться администраторами плагина достижений. Они могут создавать типы, а также изменять и выдавать любые значки.'
defaultMessage='Эти пользователи будут считаться администраторами плагина достижений. Они могут создавать типы, а также изменять и выдавать любые достижения.'
/>
</div>
</div>

View File

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

View File

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

View File

@ -36,7 +36,7 @@ const TypeSelect: React.FC<Props> = ({
const intl = useIntl();
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 confirmType = confirmDeleteTypeId ? types.find((t) => String(t.id) === confirmDeleteTypeId) : null;
Review

мб в мемо?

мб в мемо?

View File

@ -30,5 +30,31 @@
display: flex;
justify-content: center;
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}) => (
<div className='ConfirmDialog__overlay'>
<div
className='ConfirmDialog__overlay'
onClick={(e) => e.stopPropagation()}
>
<div className='ConfirmDialog'>
<p className='ConfirmDialog__text'>
{children}

View File

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

View File

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

View File

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

View File

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

View File

@ -76,7 +76,7 @@ const RHS: React.FC = () => {
>
<FormattedMessage
id='badges.rhs.all_badges'
defaultMessage='Все значки'
defaultMessage='Все достижения'
/>
</button>
{canEditType && (
@ -98,7 +98,7 @@ const RHS: React.FC = () => {
>
<FormattedMessage
id='badges.rhs.create_badge'
defaultMessage='+ Создать значок'
defaultMessage='+ Создать достижение'
/>
</button>
)}
@ -163,6 +163,7 @@ const RHS: React.FC = () => {
<UserBadges
user={currentUser}
isCurrentUser={false}
currentUserID={myUser.id}
actions={{
setRHSView: (view: RHSState) => dispatch(setRHSView(view)),
setRHSBadge: (badge: BadgeID | null) => dispatch(setRHSBadge(badge)),
@ -176,6 +177,7 @@ const RHS: React.FC = () => {
<UserBadges
user={myUser}
isCurrentUser={true}
currentUserID={myUser.id}
actions={{
setRHSView: (view: RHSState) => dispatch(setRHSView(view)),
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';
@ -7,18 +7,42 @@ import Client4 from 'mattermost-redux/client/client4';
import {UserBadge} from '../../types/badges';
import BadgeImage from '../utils/badge_image';
import {markdown} from 'utils/markdown';
import Client from '../../client/api';
import ConfirmDialog from '../confirm_dialog/confirm_dialog';
import './user_badge_row.scss';
type Props = {
badge: UserBadge;
isCurrentUser: boolean;
currentUserID: string;
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 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;
if (badge.reason) {
reason = (
@ -50,6 +74,42 @@ const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) =
</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 (
<div
className='UserBadgesRow'
@ -105,6 +165,7 @@ const UserBadgeRow: React.FC<Props> = ({badge, onClick, isCurrentUser}: Props) =
</div>
{reason}
{setStatus}
{revokeAction}
</div>
</div>
);

View File

@ -18,6 +18,7 @@ import './user_badges.scss';
type Props = {
isCurrentUser: boolean;
currentUserID: string;
user: UserProfile | null;
actions: {
setRHSView: (view: RHSState) => void;
@ -84,6 +85,17 @@ class UserBadges extends React.PureComponent<Props, State> {
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() {
if (!this.props.user) {
return (<div>
@ -104,7 +116,7 @@ class UserBadges extends React.PureComponent<Props, State> {
return (<div>
<FormattedMessage
id='badges.no_badges_yet'
defaultMessage='Значков пока нет.'
defaultMessage='Достижений пока нет.'
/>
</div>);
}
@ -113,9 +125,11 @@ class UserBadges extends React.PureComponent<Props, State> {
return (
<UserBadgeRow
isCurrentUser={this.props.isCurrentUser}
currentUserID={this.props.currentUserID}
key={badge.time}
badge={badge}
onClick={this.onBadgeClick}
onRevoke={this.onRevoke}
/>
);
});
@ -123,12 +137,12 @@ class UserBadges extends React.PureComponent<Props, State> {
const title = this.props.isCurrentUser ? (
<FormattedMessage
id='badges.rhs.my_badges'
defaultMessage='Мои значки'
defaultMessage='Мои достижения'
/>
) : (
<FormattedMessage
id='badges.rhs.user_badges'
defaultMessage='Значки @{username}'
defaultMessage='Достижения @{username}'
values={{username: this.props.user.username}}
/>
);

View File

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

View File

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

View File

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

client

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

View File

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