diff --git a/server/api.go b/server/api.go index 6e3afaa..88ca4f2 100644 --- a/server/api.go +++ b/server/api.go @@ -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) +} diff --git a/server/command.go b/server/command.go index 8be33ce..66d596e 100644 --- a/server/command.go +++ b/server/command.go @@ -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()) } diff --git a/server/i18n/en.json b/server/i18n/en.json index b556bb7..3510fd8 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -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."} -] +] \ No newline at end of file diff --git a/server/i18n/i18n.go b/server/i18n/i18n.go index 0ab687f..4231801 100644 --- a/server/i18n/i18n.go +++ b/server/i18n/i18n.go @@ -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) } diff --git a/server/i18n/ru.json b/server/i18n/ru.json index f619cfe..7b6778c 100644 --- a/server/i18n/ru.json +++ b/server/i18n/ru.json @@ -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": "У вас нет прав на отправку уведомления о выдаче в этот канал."} ] diff --git a/server/store.go b/server/store.go index 4f8f782..a8ad05b 100644 --- a/server/store.go +++ b/server/store.go @@ -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) }) diff --git a/server/store_atomic.go b/server/store_atomic.go index a09ace8..c33f2c1 100644 --- a/server/store_atomic.go +++ b/server/store_atomic.go @@ -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 { diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 17592a0..767d06e 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -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", diff --git a/webapp/i18n/ru.json b/webapp/i18n/ru.json index 059416e..476d09a 100644 --- a/webapp/i18n/ru.json +++ b/webapp/i18n/ru.json @@ -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": "Не удалось удалить тип", diff --git a/webapp/src/client/api.ts b/webapp/src/client/api.ts index 66c02fc..d5513d9 100644 --- a/webapp/src/client/api.ts +++ b/webapp/src/client/api.ts @@ -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 { + await this.doPost(`${this.url}/revokeOwnership`, req); + } + async getChannelSubscriptions(channelID: string): Promise { try { const res = await this.doGet(`${this.url}/getChannelSubscriptions/${channelID}`); diff --git a/webapp/src/components/admin/badges_admin_setting.tsx b/webapp/src/components/admin/badges_admin_setting.tsx index 02aae09..fc28d5d 100644 --- a/webapp/src/components/admin/badges_admin_setting.tsx +++ b/webapp/src/components/admin/badges_admin_setting.tsx @@ -39,7 +39,7 @@ const BadgesAdminSetting: React.FC = ({id, value, disabled, onChange, set
diff --git a/webapp/src/components/badge_modal/index.tsx b/webapp/src/components/badge_modal/index.tsx index 2a21703..4d638a3 100644 --- a/webapp/src/components/badge_modal/index.tsx +++ b/webapp/src/components/badge_modal/index.tsx @@ -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 символов)'})} />
@@ -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 символов)'})} />
@@ -398,45 +399,27 @@ const BadgeModal: React.FC = () => { {error &&
{error}
} {isEditMode && (
- {confirmDelete ? ( -
- - - - - -
- ) : ( - + {confirmDelete && ( + setConfirmDelete(false)} > - + )}
)} diff --git a/webapp/src/components/badge_modal/inline_type_form.tsx b/webapp/src/components/badge_modal/inline_type_form.tsx index aec72b8..392e991 100644 --- a/webapp/src/components/badge_modal/inline_type_form.tsx +++ b/webapp/src/components/badge_modal/inline_type_form.tsx @@ -41,7 +41,7 @@ const InlineTypeForm: React.FC = ({form, onChange}) => {
@@ -69,7 +69,7 @@ const InlineTypeForm: React.FC = ({form, onChange}) => { diff --git a/webapp/src/components/badge_modal/type_select.tsx b/webapp/src/components/badge_modal/type_select.tsx index d9cbdd3..53d3ce8 100644 --- a/webapp/src/components/badge_modal/type_select.tsx +++ b/webapp/src/components/badge_modal/type_select.tsx @@ -36,7 +36,7 @@ const TypeSelect: React.FC = ({ const intl = useIntl(); const selectedTypeName = types.find((t) => String(t.id) === badgeType)?.name || - 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; diff --git a/webapp/src/components/confirm_dialog/confirm_dialog.scss b/webapp/src/components/confirm_dialog/confirm_dialog.scss index 31d380e..bbb676d 100644 --- a/webapp/src/components/confirm_dialog/confirm_dialog.scss +++ b/webapp/src/components/confirm_dialog/confirm_dialog.scss @@ -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); + } + } } } diff --git a/webapp/src/components/confirm_dialog/confirm_dialog.tsx b/webapp/src/components/confirm_dialog/confirm_dialog.tsx index 374095e..45a0be8 100644 --- a/webapp/src/components/confirm_dialog/confirm_dialog.tsx +++ b/webapp/src/components/confirm_dialog/confirm_dialog.tsx @@ -11,7 +11,10 @@ type Props = { } const ConfirmDialog: React.FC = ({children, onConfirm, onCancel}) => ( -
+
e.stopPropagation()} + >

{children} diff --git a/webapp/src/components/grant_modal/index.tsx b/webapp/src/components/grant_modal/index.tsx index 43f9fd7..cf21611 100644 --- a/webapp/src/components/grant_modal/index.tsx +++ b/webapp/src/components/grant_modal/index.tsx @@ -45,7 +45,7 @@ const GrantModal: React.FC = () => { const [error, setError] = useState(null); const [closing, setClosing] = useState(false); - // Выбор значка + // Выбор достижения const [badgeDropdownOpen, setBadgeDropdownOpen] = useState(false); const badgeDropdownRef = useRef(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 = () => {

@@ -209,7 +209,7 @@ const GrantModal: React.FC = () => {
)} @@ -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: 'За что выдаётся достижение? (необязательно)'})} />
diff --git a/webapp/src/components/rhs/all_badges.tsx b/webapp/src/components/rhs/all_badges.tsx index a68bc8f..883d191 100644 --- a/webapp/src/components/rhs/all_badges.tsx +++ b/webapp/src/components/rhs/all_badges.tsx @@ -112,13 +112,13 @@ const AllBadges: React.FC = ({filterTypeId, filterTypeName, actions}) =>
@@ -127,7 +127,7 @@ const AllBadges: React.FC = ({filterTypeId, filterTypeName, actions}) =>
)} diff --git a/webapp/src/components/rhs/all_types_row.tsx b/webapp/src/components/rhs/all_types_row.tsx index 73c5099..c1d2ae9 100644 --- a/webapp/src/components/rhs/all_types_row.tsx +++ b/webapp/src/components/rhs/all_types_row.tsx @@ -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 = ({badgeType, onEdit, onDelete, onClick}: Pr
{badgeType.can_create?.everyone && ( @@ -71,58 +72,37 @@ const AllTypesRow: React.FC = ({badgeType, onEdit, onDelete, onClick}: Pr className='AllTypesRow__actions' onClick={(e) => e.stopPropagation()} > - {!confirmDelete && ( + + {!badgeType.is_default && ( )} - {!badgeType.is_default && ( - <> - {confirmDelete ? ( -
- - - - - -
- ) : ( - - )} - + {confirmDelete && ( + onDelete(badgeType)} + onCancel={() => setConfirmDelete(false)} + > + + )}
diff --git a/webapp/src/components/rhs/badge_details.tsx b/webapp/src/components/rhs/badge_details.tsx index c3197d1..539282a 100644 --- a/webapp/src/components/rhs/badge_details.tsx +++ b/webapp/src/components/rhs/badge_details.tsx @@ -93,7 +93,7 @@ class BadgeDetailsComponent extends React.PureComponent { return (
); } @@ -108,7 +108,7 @@ class BadgeDetailsComponent extends React.PureComponent { return (
); } diff --git a/webapp/src/components/rhs/index.tsx b/webapp/src/components/rhs/index.tsx index c5f788b..6e6c1b2 100644 --- a/webapp/src/components/rhs/index.tsx +++ b/webapp/src/components/rhs/index.tsx @@ -76,7 +76,7 @@ const RHS: React.FC = () => { > {canEditType && ( @@ -98,7 +98,7 @@ const RHS: React.FC = () => { > )} @@ -163,6 +163,7 @@ const RHS: React.FC = () => { dispatch(setRHSView(view)), setRHSBadge: (badge: BadgeID | null) => dispatch(setRHSBadge(badge)), @@ -176,6 +177,7 @@ const RHS: React.FC = () => { dispatch(setRHSView(view)), setRHSBadge: (badge: BadgeID | null) => dispatch(setRHSBadge(badge)), diff --git a/webapp/src/components/rhs/user_badge_row.scss b/webapp/src/components/rhs/user_badge_row.scss index 85f77e2..68e50d5 100644 --- a/webapp/src/components/rhs/user_badge_row.scss +++ b/webapp/src/components/rhs/user_badge_row.scss @@ -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; + } + } } diff --git a/webapp/src/components/rhs/user_badge_row.tsx b/webapp/src/components/rhs/user_badge_row.tsx index 1758277..19e5859 100644 --- a/webapp/src/components/rhs/user_badge_row.tsx +++ b/webapp/src/components/rhs/user_badge_row.tsx @@ -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 = ({badge, onClick, isCurrentUser}: Props) => { +const UserBadgeRow: React.FC = ({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 = ({badge, onClick, isCurrentUser}: Props) =
); } + + let revokeAction = null; + if (canRevoke && onRevoke) { + revokeAction = ( + + ); + if (confirmingRevoke) { + revokeAction = ( + <> + {revokeAction} + setConfirmingRevoke(false)} + > + + + + ); + } + } + return (
= ({badge, onClick, isCurrentUser}: Props) =
{reason} {setStatus} + {revokeAction} ); diff --git a/webapp/src/components/rhs/user_badges.tsx b/webapp/src/components/rhs/user_badges.tsx index 8ac0e1e..7a346b5 100644 --- a/webapp/src/components/rhs/user_badges.tsx +++ b/webapp/src/components/rhs/user_badges.tsx @@ -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 { this.props.actions.setRHSView(RHS_STATE_DETAIL); } + onRevoke = () => { + if (!this.props.user) { + return; + } + const c = new Client(); + this.setState({loading: true}); + c.getUserBadges(this.props.user.id).then((badges) => { + this.setState({badges, loading: false}); + }); + } + render() { if (!this.props.user) { return (
@@ -104,7 +116,7 @@ class UserBadges extends React.PureComponent { return (
); } @@ -113,9 +125,11 @@ class UserBadges extends React.PureComponent { return ( ); }); @@ -123,12 +137,12 @@ class UserBadges extends React.PureComponent { const title = this.props.isCurrentUser ? ( ) : ( ); diff --git a/webapp/src/components/subscription_modal/index.tsx b/webapp/src/components/subscription_modal/index.tsx index 5d5efb3..7880181 100644 --- a/webapp/src/components/subscription_modal/index.tsx +++ b/webapp/src/components/subscription_modal/index.tsx @@ -128,7 +128,7 @@ const SubscriptionModal: React.FC = () => { @@ -144,7 +144,7 @@ const SubscriptionModal: React.FC = () => { {selectedType ? selectedType.name - : intl.formatMessage({id: 'badges.subscription.field_type_placeholder', defaultMessage: 'Выберите тип значков'}) + : intl.formatMessage({id: 'badges.subscription.field_type_placeholder', defaultMessage: 'Выберите тип достижений'}) } {'▾'} diff --git a/webapp/src/components/type_modal/index.tsx b/webapp/src/components/type_modal/index.tsx index 9525919..6dde096 100644 --- a/webapp/src/components/type_modal/index.tsx +++ b/webapp/src/components/type_modal/index.tsx @@ -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 = () => {
@@ -192,7 +193,7 @@ const TypeModal: React.FC = () => { @@ -207,7 +208,7 @@ const TypeModal: React.FC = () => { @@ -226,7 +227,7 @@ const TypeModal: React.FC = () => { @@ -234,46 +235,27 @@ const TypeModal: React.FC = () => { {error &&
{error}
} {isEditMode && !editData?.is_default && (
- {confirmDelete ? ( -
- - - - - -
- ) : ( - + {confirmDelete && ( + setConfirmDelete(false)} > - + )}
)} diff --git a/webapp/src/components/user_popover/badge_list.tsx b/webapp/src/components/user_popover/badge_list.tsx index f3f9234..7dbb0d8 100644 --- a/webapp/src/components/user_popover/badge_list.tsx +++ b/webapp/src/components/user_popover/badge_list.tsx @@ -42,7 +42,7 @@ type State = { loaded?: boolean; } -const MAX_BADGES = 7; +const MAX_BADGES = 6; const BADGE_SIZE = 24; class BadgeList extends React.PureComponent { @@ -229,7 +229,7 @@ class BadgeList extends React.PureComponent {
@@ -244,7 +244,7 @@ class BadgeList extends React.PureComponent {
diff --git a/webapp/src/types/badges.ts b/webapp/src/types/badges.ts index f202da1..e347e36 100644 --- a/webapp/src/types/badges.ts +++ b/webapp/src/types/badges.ts @@ -128,3 +128,9 @@ export type SubscriptionRequest = { type_id: string; channel_id: string; } + +export type RevokeOwnershipRequest = { + badge_id: string; + user_id: string; + time: string; +}