LP-5613 #2
225
server/api.go
225
server/api.go
@ -68,6 +68,19 @@ type UpdateTypeRequest struct {
|
||||
AllowlistCanGrant string `json:"allowlist_can_grant"`
|
||||
}
|
||||
|
||||
type GrantBadgeAPIRequest struct {
|
||||
BadgeID string `json:"badge_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Reason string `json:"reason"`
|
||||
NotifyHere bool `json:"notify_here"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
}
|
||||
|
||||
type SubscriptionAPIRequest struct {
|
||||
TypeID string `json:"type_id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
}
|
||||
|
||||
type TypeWithBadgeCount struct {
|
||||
*badgesmodel.BadgeTypeDefinition
|
||||
BadgeCount int `json:"badge_count"`
|
||||
@ -100,6 +113,10 @@ func (p *Plugin) initializeAPI() {
|
||||
apiRouter.HandleFunc("/updateType", p.extractUserMiddleWare(p.apiUpdateType, ResponseTypeJSON)).Methods(http.MethodPut)
|
||||
apiRouter.HandleFunc("/deleteBadge/{badgeID}", p.extractUserMiddleWare(p.apiDeleteBadge, ResponseTypeJSON)).Methods(http.MethodDelete)
|
||||
apiRouter.HandleFunc("/deleteType/{typeID}", p.extractUserMiddleWare(p.apiDeleteType, ResponseTypeJSON)).Methods(http.MethodDelete)
|
||||
apiRouter.HandleFunc("/grantBadge", p.extractUserMiddleWare(p.apiGrantBadge, 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)
|
||||
|
||||
pluginAPIRouter.HandleFunc(badgesmodel.PluginAPIPathEnsure, checkPluginRequest(p.ensureBadges)).Methods(http.MethodPost)
|
||||
pluginAPIRouter.HandleFunc(badgesmodel.PluginAPIPathGrant, checkPluginRequest(p.grantBadge)).Methods(http.MethodPost)
|
||||
@ -1586,3 +1603,211 @@ func (p *Plugin) getPluginURL() string {
|
||||
func (p *Plugin) getDialogURL() string {
|
||||
return p.getPluginURL() + DialogPath
|
||||
}
|
||||
|
||||
func (p *Plugin) apiGrantBadge(w http.ResponseWriter, r *http.Request, userID string) {
|
||||
var req GrantBadgeAPIRequest
|
||||
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)
|
||||
|
||||
if req.BadgeID == "" {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "invalid_badge_id", Message: "Badge ID is required", StatusCode: http.StatusBadRequest,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.UserID == "" {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "invalid_user_id", Message: "User ID is required", StatusCode: http.StatusBadRequest,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
badge, err := p.store.GetBadge(badgesmodel.BadgeID(req.BadgeID))
|
||||
if err != nil {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "badge_not_found", Message: "Badge not found", StatusCode: http.StatusNotFound,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
granter, err := p.mm.User.Get(userID)
|
||||
if err != nil {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
badgeType, err := p.store.GetType(badge.Type)
|
||||
if err != nil {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "type_not_found", Message: "Badge type not found", StatusCode: http.StatusInternalServerError,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !canGrantBadge(granter, p.badgeAdminUserIDs, badge, badgeType) {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "no_permission_grant", Message: "No permission to grant this badge", StatusCode: http.StatusForbidden,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
grantToUser, err := p.mm.User.Get(req.UserID)
|
||||
if err != nil {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "user_not_found", Message: "User not found", StatusCode: http.StatusNotFound,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
shouldNotify, err := p.store.GrantBadge(badgesmodel.BadgeID(req.BadgeID), req.UserID, userID, req.Reason)
|
||||
if err != nil {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "cannot_grant_badge", Message: err.Error(), StatusCode: http.StatusInternalServerError,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if shouldNotify {
|
||||
channelID := req.ChannelID
|
||||
p.notifyGrant(badgesmodel.BadgeID(req.BadgeID), userID, grantToUser, req.NotifyHere, channelID, req.Reason)
|
||||
}
|
||||
|
||||
resp := map[string]string{"status": "ok"}
|
||||
b, _ := json.Marshal(resp)
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
|
||||
func (p *Plugin) apiCreateSubscription(w http.ResponseWriter, r *http.Request, userID string) {
|
||||
var req SubscriptionAPIRequest
|
||||
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
|
||||
}
|
||||
|
||||
u, err := p.mm.User.Get(userID)
|
||||
if err != nil {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !canCreateSubscription(u, p.badgeAdminUserIDs, req.ChannelID) {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "no_permission_subscription", Message: "No permission to manage subscriptions", StatusCode: http.StatusForbidden,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
req.TypeID = strings.TrimSpace(req.TypeID)
|
||||
if req.TypeID == "" {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "invalid_type_id", Message: "Type ID is required", StatusCode: http.StatusBadRequest,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = p.store.AddSubscription(badgesmodel.BadgeType(req.TypeID), req.ChannelID)
|
||||
if err != nil {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "cannot_create_subscription", Message: err.Error(), StatusCode: http.StatusInternalServerError,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
T := p.getT(u.Locale)
|
||||
p.mm.Post.SendEphemeralPost(userID, &model.Post{
|
||||
UserId: p.BotUserID,
|
||||
ChannelId: req.ChannelID,
|
||||
Message: T("badges.api.subscription_added", "Подписка добавлена"),
|
||||
})
|
||||
|
||||
resp := map[string]string{"status": "ok"}
|
||||
b, _ := json.Marshal(resp)
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
|
||||
func (p *Plugin) apiDeleteSubscription(w http.ResponseWriter, r *http.Request, userID string) {
|
||||
var req SubscriptionAPIRequest
|
||||
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
|
||||
}
|
||||
|
||||
u, err := p.mm.User.Get(userID)
|
||||
if err != nil {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !canCreateSubscription(u, p.badgeAdminUserIDs, req.ChannelID) {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "no_permission_subscription", Message: "No permission to manage subscriptions", StatusCode: http.StatusForbidden,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
req.TypeID = strings.TrimSpace(req.TypeID)
|
||||
if req.TypeID == "" {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "invalid_type_id", Message: "Type ID is required", StatusCode: http.StatusBadRequest,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = p.store.RemoveSubscriptions(badgesmodel.BadgeType(req.TypeID), req.ChannelID)
|
||||
if err != nil {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "cannot_delete_subscription", Message: err.Error(), StatusCode: http.StatusInternalServerError,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
T := p.getT(u.Locale)
|
||||
p.mm.Post.SendEphemeralPost(userID, &model.Post{
|
||||
UserId: p.BotUserID,
|
||||
ChannelId: req.ChannelID,
|
||||
Message: T("badges.api.subscription_removed", "Подписка удалена"),
|
||||
})
|
||||
|
||||
resp := map[string]string{"status": "ok"}
|
||||
b, _ := json.Marshal(resp)
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
|
||||
func (p *Plugin) apiGetChannelSubscriptions(w http.ResponseWriter, r *http.Request, userID string) {
|
||||
channelID := mux.Vars(r)["channelID"]
|
||||
if channelID == "" {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "invalid_request", Message: "Channel ID is required", StatusCode: http.StatusBadRequest,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
types, err := p.store.GetChannelSubscriptions(channelID)
|
||||
if err != nil {
|
||||
p.writeAPIError(w, &APIErrorResponse{
|
||||
ID: "cannot_get_types", Message: err.Error(), StatusCode: http.StatusInternalServerError,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(types)
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
|
||||
@ -98,6 +98,34 @@
|
||||
"badges.modal.allowlist_grant_help": "Users who can grant badges 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.field_reason": "Reason",
|
||||
"badges.grant.field_reason_placeholder": "Why is this badge being granted? (optional)",
|
||||
"badges.grant.notify_here": "Notify in channel",
|
||||
"badges.grant.btn_grant": "Grant",
|
||||
|
||||
"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.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_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.user_not_found": "User not found",
|
||||
"badges.error.invalid_type_id": "Badge 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.unknown": "An error occurred",
|
||||
"badges.error.cannot_get_user": "Failed to get user data",
|
||||
"badges.error.cannot_get_types": "Failed to load types",
|
||||
|
||||
@ -98,6 +98,34 @@
|
||||
"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.field_reason": "Причина",
|
||||
"badges.grant.field_reason_placeholder": "За что выдаётся значок? (необязательно)",
|
||||
"badges.grant.notify_here": "Уведомить в канале",
|
||||
"badges.grant.btn_grant": "Выдать",
|
||||
|
||||
"badges.subscription.title_create": "Добавить подписку",
|
||||
"badges.subscription.title_delete": "Удалить подписку",
|
||||
"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_user_id": "Не указан пользователь",
|
||||
"badges.error.no_permission_grant": "Недостаточно прав для выдачи этого значка",
|
||||
"badges.error.cannot_grant_badge": "Не удалось выдать значок",
|
||||
"badges.error.user_not_found": "Пользователь не найден",
|
||||
"badges.error.invalid_type_id": "Не указан тип значков",
|
||||
"badges.error.no_permission_subscription": "Недостаточно прав для управления подписками",
|
||||
"badges.error.cannot_create_subscription": "Не удалось создать подписку",
|
||||
"badges.error.cannot_delete_subscription": "Не удалось удалить подписку",
|
||||
|
||||
"badges.error.unknown": "Произошла ошибка",
|
||||
"badges.error.cannot_get_user": "Не удалось получить данные пользователя",
|
||||
"badges.error.cannot_get_types": "Не удалось загрузить типы",
|
||||
|
||||
@ -17,4 +17,8 @@ export default {
|
||||
CLOSE_CREATE_TYPE_MODAL: pluginId + '_close_create_type_modal',
|
||||
OPEN_EDIT_TYPE_MODAL: pluginId + '_open_edit_type_modal',
|
||||
CLOSE_EDIT_TYPE_MODAL: pluginId + '_close_edit_type_modal',
|
||||
OPEN_GRANT_MODAL: pluginId + '_open_grant_modal',
|
||||
CLOSE_GRANT_MODAL: pluginId + '_close_grant_modal',
|
||||
OPEN_SUBSCRIPTION_MODAL: pluginId + '_open_subscription_modal',
|
||||
CLOSE_SUBSCRIPTION_MODAL: pluginId + '_close_subscription_modal',
|
||||
};
|
||||
|
||||
@ -1,14 +1,8 @@
|
||||
import {AnyAction, Dispatch} from 'redux';
|
||||
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {GetStateFunc} from 'mattermost-redux/types/actions';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {IntegrationTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
import ActionTypes from 'action_types/';
|
||||
import {BadgeDetails, BadgeID, BadgeTypeDefinition} from 'types/badges';
|
||||
import {RHSState} from 'types/general';
|
||||
import {GrantModalData, RHSState, SubscriptionModalData} from 'types/general';
|
||||
|
||||
/**
|
||||
* Stores`showRHSPlugin` action returned by
|
||||
@ -49,35 +43,16 @@ export function setRHSType(typeId: number | null, typeName: string | null) {
|
||||
};
|
||||
}
|
||||
|
||||
export function setTriggerId(triggerId: string) {
|
||||
return {
|
||||
type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID,
|
||||
data: triggerId,
|
||||
};
|
||||
}
|
||||
|
||||
export function openGrant(user?: string, badge?: string) {
|
||||
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
||||
let command = '/badges grant';
|
||||
if (user) {
|
||||
command += ` --user ${user}`;
|
||||
}
|
||||
|
||||
if (badge) {
|
||||
command += ` --badge ${badge}`;
|
||||
}
|
||||
|
||||
clientExecuteCommand(dispatch, getState, command);
|
||||
|
||||
return (dispatch: Dispatch<AnyAction>) => {
|
||||
dispatch(openGrantModal({prefillUser: user, prefillBadgeId: badge}));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function openCreateType() {
|
||||
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
||||
const command = '/badges create type';
|
||||
clientExecuteCommand(dispatch, getState, command);
|
||||
|
||||
return (dispatch: Dispatch<AnyAction>) => {
|
||||
dispatch(openCreateTypeModal());
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
@ -121,43 +96,33 @@ export function closeEditTypeModal() {
|
||||
return {type: ActionTypes.CLOSE_EDIT_TYPE_MODAL};
|
||||
}
|
||||
|
||||
export function openAddSubscription() {
|
||||
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
||||
const command = '/badges subscription create';
|
||||
clientExecuteCommand(dispatch, getState, command);
|
||||
export function openGrantModal(data?: GrantModalData) {
|
||||
return {type: ActionTypes.OPEN_GRANT_MODAL, data: data || {}};
|
||||
}
|
||||
|
||||
export function closeGrantModal() {
|
||||
return {type: ActionTypes.CLOSE_GRANT_MODAL};
|
||||
}
|
||||
|
||||
export function openSubscriptionModal(data: SubscriptionModalData) {
|
||||
return {type: ActionTypes.OPEN_SUBSCRIPTION_MODAL, data};
|
||||
}
|
||||
|
||||
export function closeSubscriptionModal() {
|
||||
return {type: ActionTypes.CLOSE_SUBSCRIPTION_MODAL};
|
||||
}
|
||||
|
||||
export function openAddSubscription() {
|
||||
return (dispatch: Dispatch<AnyAction>) => {
|
||||
dispatch(openSubscriptionModal({mode: 'create'}));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function openRemoveSubscription() {
|
||||
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
||||
const command = '/badges subscription remove';
|
||||
clientExecuteCommand(dispatch, getState, command);
|
||||
|
||||
return (dispatch: Dispatch<AnyAction>) => {
|
||||
dispatch(openSubscriptionModal({mode: 'delete'}));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export async function clientExecuteCommand(dispatch: Dispatch<AnyAction>, getState: GetStateFunc, command: string) {
|
||||
let currentChannel = getCurrentChannel(getState());
|
||||
const currentTeamId = getCurrentTeamId(getState());
|
||||
|
||||
// Default to town square if there is no current channel (i.e., if Mattermost has not yet loaded)
|
||||
if (!currentChannel) {
|
||||
currentChannel = await Client4.getChannelByName(currentTeamId, 'town-square');
|
||||
}
|
||||
|
||||
const args = {
|
||||
channel_id: currentChannel?.id,
|
||||
team_id: currentTeamId,
|
||||
};
|
||||
|
||||
try {
|
||||
//@ts-ignore Typing in mattermost-redux is wrong
|
||||
const data = await Client4.executeCommand(command, args);
|
||||
dispatch(setTriggerId(data?.trigger_id));
|
||||
} catch (error) {
|
||||
console.error(error); //eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, UpdateBadgeRequest, UpdateTypeRequest, UserBadge} from 'types/badges';
|
||||
import {AllBadgesBadge, Badge, BadgeDetails, BadgeID, BadgeTypeDefinition, CreateBadgeRequest, CreateTypeRequest, GetTypesResponse, GrantBadgeRequest, SubscriptionRequest, UpdateBadgeRequest, UpdateTypeRequest, UserBadge} from 'types/badges';
|
||||
|
||||
export default class Client {
|
||||
private url: string;
|
||||
@ -74,6 +74,27 @@ export default class Client {
|
||||
await this.doDelete(`${this.url}/deleteType/${typeID}`);
|
||||
}
|
||||
|
||||
async grantBadge(req: GrantBadgeRequest): Promise<void> {
|
||||
await this.doPost(`${this.url}/grantBadge`, req);
|
||||
}
|
||||
|
||||
async createSubscription(req: SubscriptionRequest): Promise<void> {
|
||||
await this.doPost(`${this.url}/createSubscription`, req);
|
||||
}
|
||||
|
||||
async deleteSubscription(req: SubscriptionRequest): Promise<void> {
|
||||
await this.doPost(`${this.url}/deleteSubscription`, req);
|
||||
}
|
||||
|
||||
async getChannelSubscriptions(channelID: string): Promise<BadgeTypeDefinition[]> {
|
||||
try {
|
||||
const res = await this.doGet(`${this.url}/getChannelSubscriptions/${channelID}`);
|
||||
return res as BadgeTypeDefinition[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private doGet = async (url: string, headers: {[x:string]: string} = {}) => {
|
||||
headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset());
|
||||
|
||||
|
||||
@ -1,3 +1,37 @@
|
||||
@keyframes badgeModalBackdropIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes badgeModalBackdropOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes badgeModalDialogIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes badgeModalDialogOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
}
|
||||
|
||||
.BadgeModal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@ -16,6 +50,7 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
animation: badgeModalBackdropIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
&__dialog {
|
||||
@ -30,6 +65,27 @@
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: badgeModalDialogIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
&--closing {
|
||||
.BadgeModal__backdrop {
|
||||
animation: badgeModalBackdropOut 0.15s ease-in forwards;
|
||||
}
|
||||
|
||||
.BadgeModal__dialog {
|
||||
animation: badgeModalDialogOut 0.15s ease-in forwards;
|
||||
}
|
||||
}
|
||||
|
||||
&--compact {
|
||||
.BadgeModal__body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.BadgeModal__dialog {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
@ -66,6 +122,12 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.grant-intro {
|
||||
font-size: 14px;
|
||||
margin: 0 0 16px;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@ -84,6 +146,11 @@
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.64;
|
||||
|
||||
.required {
|
||||
color: var(--error-text, #d24b4e);
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
> input[type='text'],
|
||||
|
||||
@ -60,6 +60,7 @@ const BadgeModal: React.FC = () => {
|
||||
const [confirmDeleteTypeId, setConfirmDeleteTypeId] = useState<string | null>(null);
|
||||
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false);
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
const [closing, setClosing] = useState(false);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -114,15 +115,21 @@ const BadgeModal: React.FC = () => {
|
||||
setLoading(false);
|
||||
}, [isOpen, isEditMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
const doClose = useCallback(() => {
|
||||
if (createVisible) {
|
||||
dispatch(closeCreateBadgeModal());
|
||||
}
|
||||
if (editData) {
|
||||
dispatch(closeEditBadgeModal());
|
||||
}
|
||||
setClosing(false);
|
||||
}, [dispatch, createVisible, editData]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setClosing(true);
|
||||
setTimeout(doClose, 150);
|
||||
}, [doClose]);
|
||||
|
||||
const handleTypeSelect = useCallback((val: string) => {
|
||||
if (val === NEW_TYPE_VALUE) {
|
||||
setShowCreateType(true);
|
||||
@ -240,7 +247,7 @@ const BadgeModal: React.FC = () => {
|
||||
}
|
||||
}, [editData, confirmDelete, handleClose, intl, dispatch]);
|
||||
|
||||
if (!isOpen) {
|
||||
if (!isOpen && !closing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -253,7 +260,7 @@ const BadgeModal: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className='BadgeModal'
|
||||
className={'BadgeModal' + (closing ? ' BadgeModal--closing' : '')}
|
||||
ref={modalRef}
|
||||
>
|
||||
<div
|
||||
@ -280,6 +287,7 @@ const BadgeModal: React.FC = () => {
|
||||
id='badges.modal.field_name'
|
||||
defaultMessage='Название'
|
||||
/>
|
||||
<span className='required'>{'*'}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
@ -309,6 +317,7 @@ const BadgeModal: React.FC = () => {
|
||||
id='badges.modal.field_image'
|
||||
defaultMessage='Эмодзи'
|
||||
/>
|
||||
<span className='required'>{'*'}</span>
|
||||
</label>
|
||||
<div className='emoji-input'>
|
||||
<button
|
||||
@ -351,6 +360,7 @@ const BadgeModal: React.FC = () => {
|
||||
id='badges.modal.field_type'
|
||||
defaultMessage='Тип'
|
||||
/>
|
||||
<span className='required'>{'*'}</span>
|
||||
</label>
|
||||
<TypeSelect
|
||||
types={types}
|
||||
|
||||
@ -21,6 +21,7 @@ const InlineTypeForm: React.FC<Props> = ({form, onChange}) => {
|
||||
id='badges.modal.new_type_name'
|
||||
defaultMessage='Название типа'
|
||||
/>
|
||||
<span className='required'>{'*'}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
|
||||
292
webapp/src/components/grant_modal/index.tsx
Normal file
292
webapp/src/components/grant_modal/index.tsx
Normal file
@ -0,0 +1,292 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/common';
|
||||
import {GlobalState} from 'mattermost-redux/types/store';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import {closeGrantModal} from 'actions/actions';
|
||||
import {getGrantModalData} from 'selectors';
|
||||
import {AllBadgesBadge} from 'types/badges';
|
||||
import Client from 'client/api';
|
||||
import {getServerErrorId, getUserDisplayName} from 'utils/helpers';
|
||||
import CloseIcon from 'components/icons/close_icon';
|
||||
import RenderEmoji from 'components/utils/emoji';
|
||||
|
||||
type GrantFormData = {
|
||||
badgeId: string;
|
||||
userId: string;
|
||||
userDisplayName: string;
|
||||
reason: string;
|
||||
notifyHere: boolean;
|
||||
}
|
||||
|
||||
const emptyForm: GrantFormData = {
|
||||
badgeId: '',
|
||||
userId: '',
|
||||
userDisplayName: '',
|
||||
reason: '',
|
||||
notifyHere: false,
|
||||
};
|
||||
|
||||
const GrantModal: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const modalData = useSelector(getGrantModalData);
|
||||
const channelId = useSelector((state: GlobalState) => getCurrentChannelId(state));
|
||||
const isOpen = modalData !== null;
|
||||
const hasFixedUser = Boolean(modalData?.prefillUser);
|
||||
|
||||
const [form, setForm] = useState<GrantFormData>(emptyForm);
|
||||
const [badges, setBadges] = useState<AllBadgesBadge[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
||||
// Выбор значка
|
||||
const [badgeDropdownOpen, setBadgeDropdownOpen] = useState(false);
|
||||
const badgeDropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const updateForm = useCallback((updates: Partial<GrantFormData>) => {
|
||||
setForm((prev) => ({...prev, ...updates}));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Всегда очищаем форму при открытии
|
||||
setForm(emptyForm);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
setBadgeDropdownOpen(false);
|
||||
|
||||
const fetchBadges = async () => {
|
||||
const client = new Client();
|
||||
const allBadges = await client.getAllBadges();
|
||||
setBadges(allBadges);
|
||||
};
|
||||
fetchBadges();
|
||||
|
||||
// Prefill значка, если передан
|
||||
if (modalData?.prefillBadgeId) {
|
||||
setForm((prev) => ({...prev, badgeId: modalData.prefillBadgeId || ''}));
|
||||
}
|
||||
|
||||
// Prefill пользователя, если передан
|
||||
if (modalData?.prefillUser) {
|
||||
Client4.getUserByUsername(modalData.prefillUser).then((user) => {
|
||||
|
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
userId: user.id,
|
||||
userDisplayName: getUserDisplayName(user) || user.username,
|
||||
}));
|
||||
}).catch(() => {
|
||||
// Если пользователь не найден — игнорируем
|
||||
});
|
||||
}
|
||||
}, [isOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Закрытие выпадающих списков при клике снаружи
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (badgeDropdownRef.current && !badgeDropdownRef.current.contains(e.target as Node)) {
|
||||
setBadgeDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const doClose = useCallback(() => {
|
||||
dispatch(closeGrantModal());
|
||||
setClosing(false);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setClosing(true);
|
||||
setTimeout(doClose, 150);
|
||||
|
vladimir.khablak
commented
на твое усмотрение - вынести число в константу на твое усмотрение - вынести число в константу
|
||||
}, [doClose]);
|
||||
|
||||
const handleBadgeSelect = (badgeId: string) => {
|
||||
updateForm({badgeId});
|
||||
setBadgeDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const client = new Client();
|
||||
await client.grantBadge({
|
||||
badge_id: form.badgeId,
|
||||
user_id: form.userId,
|
||||
reason: form.reason.trim(),
|
||||
notify_here: form.notifyHere,
|
||||
channel_id: channelId,
|
||||
});
|
||||
handleClose();
|
||||
} catch (err) {
|
||||
setError(intl.formatMessage({id: 'badges.error.' + (getServerErrorId(err) || 'unknown'), defaultMessage: 'Произошла ошибка'}));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [form, channelId, handleClose, intl]);
|
||||
|
||||
if (!isOpen && !closing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedBadge = badges.find((b) => String(b.id) === form.badgeId);
|
||||
|
||||
return (
|
||||
<div className={'BadgeModal' + (closing ? ' BadgeModal--closing' : '')}>
|
||||
<div
|
||||
className='BadgeModal__backdrop'
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<div className='BadgeModal__dialog'>
|
||||
<div className='BadgeModal__header'>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id='badges.grant.title'
|
||||
defaultMessage='Выдать значок'
|
||||
/>
|
||||
</h4>
|
||||
<button
|
||||
className='close-btn'
|
||||
onClick={handleClose}
|
||||
>
|
||||
<CloseIcon/>
|
||||
</button>
|
||||
</div>
|
||||
<div className='BadgeModal__body'>
|
||||
{hasFixedUser && form.userDisplayName && (
|
||||
<p className='grant-intro'>
|
||||
<FormattedMessage
|
||||
id='badges.grant.intro'
|
||||
defaultMessage='Выдать значок пользователю @{username}'
|
||||
values={{username: modalData?.prefillUser || ''}}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='badges.grant.field_badge'
|
||||
defaultMessage='Значок'
|
||||
/>
|
||||
<span className='required'>{'*'}</span>
|
||||
</label>
|
||||
<div
|
||||
className='type-select'
|
||||
ref={badgeDropdownRef}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='type-select__trigger'
|
||||
onClick={() => setBadgeDropdownOpen(!badgeDropdownOpen)}
|
||||
>
|
||||
<span className='type-select__value'>
|
||||
{selectedBadge ? (
|
||||
<>
|
||||
<RenderEmoji
|
||||
emojiName={selectedBadge.image}
|
||||
size={16}
|
||||
/>
|
||||
{' '}{selectedBadge.name}
|
||||
</>
|
||||
) : intl.formatMessage({id: 'badges.grant.field_badge_placeholder', defaultMessage: 'Выберите значок'})}
|
||||
</span>
|
||||
<span className='type-select__arrow'>{'▾'}</span>
|
||||
</button>
|
||||
{badgeDropdownOpen && (
|
||||
<div className='type-select__dropdown'>
|
||||
{badges.length === 0 && (
|
||||
<div className='type-select__option'>
|
||||
<FormattedMessage
|
||||
id='badges.grant.no_badges'
|
||||
defaultMessage='Нет доступных значков'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{badges.map((badge) => (
|
||||
<div
|
||||
key={badge.id}
|
||||
className={'type-select__option' + (String(badge.id) === form.badgeId ? ' type-select__option--selected' : '')}
|
||||
onClick={() => handleBadgeSelect(String(badge.id))}
|
||||
>
|
||||
<span className='type-select__option-name'>
|
||||
<RenderEmoji
|
||||
emojiName={badge.image}
|
||||
size={16}
|
||||
/>
|
||||
{' '}{badge.name}
|
||||
</span>
|
||||
<span style={{opacity: 0.56, fontSize: '12px'}}>{badge.type_name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='badges.grant.field_reason'
|
||||
defaultMessage='Причина'
|
||||
/>
|
||||
</label>
|
||||
<textarea
|
||||
value={form.reason}
|
||||
onChange={(e) => updateForm({reason: e.target.value})}
|
||||
maxLength={200}
|
||||
placeholder={intl.formatMessage({id: 'badges.grant.field_reason_placeholder', defaultMessage: 'За что выдаётся значок? (необязательно)'})}
|
||||
/>
|
||||
</div>
|
||||
<div className='checkbox-group'>
|
||||
<input
|
||||
type='checkbox'
|
||||
id='grantNotifyHere'
|
||||
checked={form.notifyHere}
|
||||
onChange={(e) => updateForm({notifyHere: e.target.checked})}
|
||||
/>
|
||||
<label htmlFor='grantNotifyHere'>
|
||||
<FormattedMessage
|
||||
id='badges.grant.notify_here'
|
||||
defaultMessage='Уведомить в канале'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{error && <div className='error-message'>{error}</div>}
|
||||
</div>
|
||||
<div className='BadgeModal__footer'>
|
||||
<button
|
||||
className='btn btn--cancel'
|
||||
onClick={handleClose}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='badges.modal.btn_cancel'
|
||||
defaultMessage='Отмена'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className='btn btn--primary'
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || !form.badgeId || !form.userId}
|
||||
>
|
||||
{loading
|
||||
? intl.formatMessage({id: 'badges.modal.btn_creating', defaultMessage: 'Сохранение...'})
|
||||
: intl.formatMessage({id: 'badges.grant.btn_grant', defaultMessage: 'Выдать'})
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrantModal;
|
||||
@ -41,12 +41,14 @@ const RHS: React.FC = () => {
|
||||
|
||||
const [canEditType, setCanEditType] = useState(false);
|
||||
const [canCreateType, setCanCreateType] = useState(false);
|
||||
const [canCreateBadge, setCanCreateBadge] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const client = new Client();
|
||||
client.getTypes().then((resp) => {
|
||||
setCanEditType(resp.can_edit_type);
|
||||
setCanCreateType(resp.can_create_type);
|
||||
setCanCreateBadge(resp.types.length > 0 || resp.can_create_type);
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -89,7 +91,7 @@ const RHS: React.FC = () => {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{currentView === RHS_STATE_ALL && (
|
||||
{currentView === RHS_STATE_ALL && canCreateBadge && (
|
||||
<button
|
||||
className='AllBadges__createButton'
|
||||
onClick={handleCreateBadge}
|
||||
|
||||
206
webapp/src/components/subscription_modal/index.tsx
Normal file
206
webapp/src/components/subscription_modal/index.tsx
Normal file
@ -0,0 +1,206 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/common';
|
||||
import {GlobalState} from 'mattermost-redux/types/store';
|
||||
|
||||
import {closeSubscriptionModal} from 'actions/actions';
|
||||
import {getSubscriptionModalData} from 'selectors';
|
||||
import {BadgeTypeDefinition} from 'types/badges';
|
||||
import Client from 'client/api';
|
||||
import {getServerErrorId} from 'utils/helpers';
|
||||
import CloseIcon from 'components/icons/close_icon';
|
||||
|
||||
const SubscriptionModal: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const modalData = useSelector(getSubscriptionModalData);
|
||||
const channelId = useSelector((state: GlobalState) => getCurrentChannelId(state));
|
||||
const isOpen = modalData !== null;
|
||||
const isDeleteMode = modalData?.mode === 'delete';
|
||||
|
||||
const [selectedTypeId, setSelectedTypeId] = useState('');
|
||||
const [types, setTypes] = useState<BadgeTypeDefinition[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [closing, setClosing] = useState(false);
|
||||
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false);
|
||||
const typeDropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Закрытие дропдауна при клике снаружи
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (typeDropdownRef.current && !typeDropdownRef.current.contains(e.target as Node)) {
|
||||
setTypeDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
const fetchTypes = async () => {
|
||||
const client = new Client();
|
||||
const subs = await client.getChannelSubscriptions(channelId);
|
||||
if (isDeleteMode) {
|
||||
setTypes(subs);
|
||||
} else {
|
||||
const resp = await client.getTypes();
|
||||
const subscribedIds = new Set(subs.map((s) => String(s.id)));
|
||||
setTypes(resp.types.filter((t) => !subscribedIds.has(String(t.id))));
|
||||
}
|
||||
};
|
||||
fetchTypes();
|
||||
setSelectedTypeId('');
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
setTypeDropdownOpen(false);
|
||||
}, [isOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const doClose = useCallback(() => {
|
||||
dispatch(closeSubscriptionModal());
|
||||
setClosing(false);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setClosing(true);
|
||||
setTimeout(doClose, 150);
|
||||
}, [doClose]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (!selectedTypeId) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const client = new Client();
|
||||
const req = {type_id: selectedTypeId, channel_id: channelId};
|
||||
if (isDeleteMode) {
|
||||
await client.deleteSubscription(req);
|
||||
} else {
|
||||
await client.createSubscription(req);
|
||||
}
|
||||
handleClose();
|
||||
} catch (err) {
|
||||
setError(intl.formatMessage({id: 'badges.error.' + (getServerErrorId(err) || 'unknown'), defaultMessage: 'Произошла ошибка'}));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedTypeId, channelId, isDeleteMode, handleClose, intl]);
|
||||
|
||||
if (!isOpen && !closing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = isDeleteMode
|
||||
? intl.formatMessage({id: 'badges.subscription.title_delete', defaultMessage: 'Удалить подписку'})
|
||||
: intl.formatMessage({id: 'badges.subscription.title_create', defaultMessage: 'Добавить подписку'});
|
||||
const submitLabel = isDeleteMode
|
||||
? intl.formatMessage({id: 'badges.subscription.btn_delete', defaultMessage: 'Удалить'})
|
||||
: intl.formatMessage({id: 'badges.subscription.btn_create', defaultMessage: 'Добавить'});
|
||||
|
||||
const selectedType = types.find((t) => String(t.id) === selectedTypeId);
|
||||
|
||||
return (
|
||||
<div className={'BadgeModal BadgeModal--compact' + (closing ? ' BadgeModal--closing' : '')}>
|
||||
<div
|
||||
className='BadgeModal__backdrop'
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<div className='BadgeModal__dialog'>
|
||||
<div className='BadgeModal__header'>
|
||||
<h4>{title}</h4>
|
||||
<button
|
||||
className='close-btn'
|
||||
onClick={handleClose}
|
||||
>
|
||||
<CloseIcon/>
|
||||
</button>
|
||||
</div>
|
||||
<div className='BadgeModal__body'>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='badges.subscription.field_type'
|
||||
defaultMessage='Тип значков'
|
||||
/>
|
||||
<span className='required'>{'*'}</span>
|
||||
</label>
|
||||
<div
|
||||
className='type-select'
|
||||
ref={typeDropdownRef}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='type-select__trigger'
|
||||
onClick={() => setTypeDropdownOpen(!typeDropdownOpen)}
|
||||
>
|
||||
<span className='type-select__value'>
|
||||
{selectedType
|
||||
? selectedType.name
|
||||
: intl.formatMessage({id: 'badges.subscription.field_type_placeholder', defaultMessage: 'Выберите тип значков'})
|
||||
}
|
||||
</span>
|
||||
<span className='type-select__arrow'>{'▾'}</span>
|
||||
</button>
|
||||
{typeDropdownOpen && (
|
||||
<div className='type-select__dropdown'>
|
||||
{types.length === 0 && (
|
||||
<div className='type-select__option'>
|
||||
<FormattedMessage
|
||||
id='badges.subscription.no_types'
|
||||
defaultMessage='Нет доступных типов'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{types.map((t) => (
|
||||
<div
|
||||
key={t.id}
|
||||
className={'type-select__option' + (String(t.id) === selectedTypeId ? ' type-select__option--selected' : '')}
|
||||
onClick={() => {
|
||||
setSelectedTypeId(String(t.id));
|
||||
setTypeDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className='type-select__option-name'>{t.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{error && <div className='error-message'>{error}</div>}
|
||||
</div>
|
||||
<div className='BadgeModal__footer'>
|
||||
<button
|
||||
className='btn btn--cancel'
|
||||
onClick={handleClose}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='badges.modal.btn_cancel'
|
||||
defaultMessage='Отмена'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className={isDeleteMode ? 'btn btn--danger' : 'btn btn--primary'}
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || !selectedTypeId}
|
||||
>
|
||||
{loading
|
||||
? intl.formatMessage({id: 'badges.modal.btn_creating', defaultMessage: 'Сохранение...'})
|
||||
: submitLabel
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscriptionModal;
|
||||
@ -31,6 +31,7 @@ const TypeModal: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
||||
const updateForm = useCallback((updates: Partial<TypeFormData>) => {
|
||||
setForm((prev) => ({...prev, ...updates}));
|
||||
@ -57,15 +58,21 @@ const TypeModal: React.FC = () => {
|
||||
setLoading(false);
|
||||
}, [isOpen, isEditMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
const doClose = useCallback(() => {
|
||||
if (createVisible) {
|
||||
dispatch(closeCreateTypeModal());
|
||||
}
|
||||
if (editData) {
|
||||
dispatch(closeEditTypeModal());
|
||||
}
|
||||
setClosing(false);
|
||||
}, [dispatch, createVisible, editData]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setClosing(true);
|
||||
setTimeout(doClose, 150);
|
||||
}, [doClose]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@ -112,7 +119,7 @@ const TypeModal: React.FC = () => {
|
||||
}
|
||||
}, [editData, confirmDelete, handleClose, intl]);
|
||||
|
||||
if (!isOpen) {
|
||||
if (!isOpen && !closing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -124,7 +131,7 @@ const TypeModal: React.FC = () => {
|
||||
: intl.formatMessage({id: 'badges.modal.btn_create', defaultMessage: 'Создать'});
|
||||
|
||||
return (
|
||||
<div className='BadgeModal'>
|
||||
<div className={'BadgeModal' + (closing ? ' BadgeModal--closing' : '')}>
|
||||
<div
|
||||
className='BadgeModal__backdrop'
|
||||
onClick={handleClose}
|
||||
@ -146,6 +153,7 @@ const TypeModal: React.FC = () => {
|
||||
id='badges.modal.field_name'
|
||||
defaultMessage='Название'
|
||||
/>
|
||||
<span className='required'>{'*'}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
@ -184,7 +192,7 @@ const TypeModal: React.FC = () => {
|
||||
<span className='form-group__help'>
|
||||
<FormattedMessage
|
||||
id='badges.modal.allowlist_create_help'
|
||||
defaultMessage='Пользователи, кото<EFBFBD><EFBFBD>ые могут создавать значки этого типа.'
|
||||
defaultMessage='Пользователи, которые могут создавать значки этого типа.'
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -22,4 +22,6 @@ export const initialState: PluginState = {
|
||||
editBadgeModalData: null,
|
||||
createTypeModalVisible: false,
|
||||
editTypeModalData: null,
|
||||
grantModalData: null,
|
||||
subscriptionModalData: null,
|
||||
};
|
||||
|
||||
@ -19,6 +19,8 @@ import {openAddSubscription, openCreateBadge, openCreateType, openRemoveSubscrip
|
||||
import RHSComponent from 'components/rhs';
|
||||
import BadgeModal from 'components/badge_modal';
|
||||
import TypeModal from 'components/type_modal';
|
||||
import GrantModal from 'components/grant_modal';
|
||||
import SubscriptionModal from 'components/subscription_modal';
|
||||
|
||||
import ChannelHeaderButton from 'components/channel_header_button';
|
||||
|
||||
@ -64,6 +66,8 @@ export default class Plugin {
|
||||
|
||||
registry.registerRootComponent(withIntl(BadgeModal));
|
||||
registry.registerRootComponent(withIntl(TypeModal));
|
||||
registry.registerRootComponent(withIntl(GrantModal));
|
||||
registry.registerRootComponent(withIntl(SubscriptionModal));
|
||||
|
||||
const locale = getCurrentUser(store.getState())?.locale || 'ru';
|
||||
const messages = getTranslations(locale);
|
||||
|
||||
@ -103,6 +103,28 @@ function editTypeModalData(state = null, action: GenericAction) {
|
||||
}
|
||||
}
|
||||
|
||||
function grantModalData(state = null, action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.OPEN_GRANT_MODAL:
|
||||
return action.data || {};
|
||||
case ActionTypes.CLOSE_GRANT_MODAL:
|
||||
return null;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function subscriptionModalData(state = null, action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.OPEN_SUBSCRIPTION_MODAL:
|
||||
return action.data;
|
||||
case ActionTypes.CLOSE_SUBSCRIPTION_MODAL:
|
||||
return null;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
showRHS,
|
||||
rhsView,
|
||||
@ -114,4 +136,6 @@ export default combineReducers({
|
||||
editBadgeModalData,
|
||||
createTypeModalVisible,
|
||||
editTypeModalData,
|
||||
grantModalData,
|
||||
subscriptionModalData,
|
||||
});
|
||||
|
||||
@ -85,3 +85,17 @@ export const getEditTypeModalData = createSelector(
|
||||
return state.editTypeModalData;
|
||||
},
|
||||
);
|
||||
|
||||
export const getGrantModalData = createSelector(
|
||||
getPluginState,
|
||||
(state) => {
|
||||
return state.grantModalData;
|
||||
},
|
||||
);
|
||||
|
||||
export const getSubscriptionModalData = createSelector(
|
||||
getPluginState,
|
||||
(state) => {
|
||||
return state.subscriptionModalData;
|
||||
},
|
||||
);
|
||||
|
||||
@ -115,3 +115,16 @@ export type UpdateTypeRequest = {
|
||||
allowlist_can_create: string;
|
||||
allowlist_can_grant: string;
|
||||
}
|
||||
|
||||
export type GrantBadgeRequest = {
|
||||
badge_id: string;
|
||||
user_id: string;
|
||||
reason: string;
|
||||
notify_here: boolean;
|
||||
channel_id: string;
|
||||
}
|
||||
|
||||
export type SubscriptionRequest = {
|
||||
type_id: string;
|
||||
channel_id: string;
|
||||
}
|
||||
|
||||
@ -2,6 +2,15 @@ import {BadgeDetails, BadgeID, BadgeTypeDefinition} from './badges';
|
||||
|
||||
export type RHSState = string;
|
||||
|
||||
export type GrantModalData = {
|
||||
prefillUser?: string;
|
||||
prefillBadgeId?: string;
|
||||
}
|
||||
|
||||
export type SubscriptionModalData = {
|
||||
mode: 'create' | 'delete';
|
||||
}
|
||||
|
||||
export type PluginState = {
|
||||
showRHS: (() => void)| null;
|
||||
rhsView: RHSState;
|
||||
@ -13,4 +22,6 @@ export type PluginState = {
|
||||
editBadgeModalData: BadgeDetails | null;
|
||||
createTypeModalVisible: boolean;
|
||||
editTypeModalData: BadgeTypeDefinition | null;
|
||||
grantModalData: GrantModalData | null;
|
||||
subscriptionModalData: SubscriptionModalData | null;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user
получается что есть api а есть еще какое-то апи, на твое усмотрение можно запихнуть в api плагина