replace interactive dialogs with custom modals for grant and subscription flows
This commit is contained in:
parent
9f4b2218b0
commit
0d582ec803
225
server/api.go
225
server/api.go
@ -68,6 +68,19 @@ type UpdateTypeRequest struct {
|
|||||||
AllowlistCanGrant string `json:"allowlist_can_grant"`
|
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 {
|
type TypeWithBadgeCount struct {
|
||||||
*badgesmodel.BadgeTypeDefinition
|
*badgesmodel.BadgeTypeDefinition
|
||||||
BadgeCount int `json:"badge_count"`
|
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("/updateType", p.extractUserMiddleWare(p.apiUpdateType, ResponseTypeJSON)).Methods(http.MethodPut)
|
||||||
apiRouter.HandleFunc("/deleteBadge/{badgeID}", p.extractUserMiddleWare(p.apiDeleteBadge, ResponseTypeJSON)).Methods(http.MethodDelete)
|
apiRouter.HandleFunc("/deleteBadge/{badgeID}", p.extractUserMiddleWare(p.apiDeleteBadge, ResponseTypeJSON)).Methods(http.MethodDelete)
|
||||||
apiRouter.HandleFunc("/deleteType/{typeID}", p.extractUserMiddleWare(p.apiDeleteType, ResponseTypeJSON)).Methods(http.MethodDelete)
|
apiRouter.HandleFunc("/deleteType/{typeID}", p.extractUserMiddleWare(p.apiDeleteType, ResponseTypeJSON)).Methods(http.MethodDelete)
|
||||||
|
apiRouter.HandleFunc("/grantBadge", p.extractUserMiddleWare(p.apiGrantBadge, ResponseTypeJSON)).Methods(http.MethodPost)
|
||||||
|
apiRouter.HandleFunc("/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.PluginAPIPathEnsure, checkPluginRequest(p.ensureBadges)).Methods(http.MethodPost)
|
||||||
pluginAPIRouter.HandleFunc(badgesmodel.PluginAPIPathGrant, checkPluginRequest(p.grantBadge)).Methods(http.MethodPost)
|
pluginAPIRouter.HandleFunc(badgesmodel.PluginAPIPathGrant, checkPluginRequest(p.grantBadge)).Methods(http.MethodPost)
|
||||||
@ -1586,3 +1603,211 @@ func (p *Plugin) getPluginURL() string {
|
|||||||
func (p *Plugin) getDialogURL() string {
|
func (p *Plugin) getDialogURL() string {
|
||||||
return p.getPluginURL() + DialogPath
|
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_grant_help": "Users who can grant badges of this type.",
|
||||||
"badges.modal.allowlist_placeholder": "user-1, user-2, user-3",
|
"badges.modal.allowlist_placeholder": "user-1, user-2, user-3",
|
||||||
|
|
||||||
|
"badges.grant.title": "Grant Badge",
|
||||||
|
"badges.grant.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.unknown": "An error occurred",
|
||||||
"badges.error.cannot_get_user": "Failed to get user data",
|
"badges.error.cannot_get_user": "Failed to get user data",
|
||||||
"badges.error.cannot_get_types": "Failed to load types",
|
"badges.error.cannot_get_types": "Failed to load types",
|
||||||
|
|||||||
@ -98,6 +98,34 @@
|
|||||||
"badges.modal.allowlist_grant_help": "Пользователи, которые могут выдавать значки этого типа.",
|
"badges.modal.allowlist_grant_help": "Пользователи, которые могут выдавать значки этого типа.",
|
||||||
"badges.modal.allowlist_placeholder": "user-1, user-2, user-3",
|
"badges.modal.allowlist_placeholder": "user-1, user-2, user-3",
|
||||||
|
|
||||||
|
"badges.grant.title": "Выдать значок",
|
||||||
|
"badges.grant.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.unknown": "Произошла ошибка",
|
||||||
"badges.error.cannot_get_user": "Не удалось получить данные пользователя",
|
"badges.error.cannot_get_user": "Не удалось получить данные пользователя",
|
||||||
"badges.error.cannot_get_types": "Не удалось загрузить типы",
|
"badges.error.cannot_get_types": "Не удалось загрузить типы",
|
||||||
|
|||||||
@ -17,4 +17,8 @@ export default {
|
|||||||
CLOSE_CREATE_TYPE_MODAL: pluginId + '_close_create_type_modal',
|
CLOSE_CREATE_TYPE_MODAL: pluginId + '_close_create_type_modal',
|
||||||
OPEN_EDIT_TYPE_MODAL: pluginId + '_open_edit_type_modal',
|
OPEN_EDIT_TYPE_MODAL: pluginId + '_open_edit_type_modal',
|
||||||
CLOSE_EDIT_TYPE_MODAL: pluginId + '_close_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 {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 ActionTypes from 'action_types/';
|
||||||
import {BadgeDetails, BadgeID, BadgeTypeDefinition} from 'types/badges';
|
import {BadgeDetails, BadgeID, BadgeTypeDefinition} from 'types/badges';
|
||||||
import {RHSState} from 'types/general';
|
import {GrantModalData, RHSState, SubscriptionModalData} from 'types/general';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores`showRHSPlugin` action returned by
|
* 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) {
|
export function openGrant(user?: string, badge?: string) {
|
||||||
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
return (dispatch: Dispatch<AnyAction>) => {
|
||||||
let command = '/badges grant';
|
dispatch(openGrantModal({prefillUser: user, prefillBadgeId: badge}));
|
||||||
if (user) {
|
|
||||||
command += ` --user ${user}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (badge) {
|
|
||||||
command += ` --badge ${badge}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
clientExecuteCommand(dispatch, getState, command);
|
|
||||||
|
|
||||||
return {data: true};
|
return {data: true};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openCreateType() {
|
export function openCreateType() {
|
||||||
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
return (dispatch: Dispatch<AnyAction>) => {
|
||||||
const command = '/badges create type';
|
dispatch(openCreateTypeModal());
|
||||||
clientExecuteCommand(dispatch, getState, command);
|
|
||||||
|
|
||||||
return {data: true};
|
return {data: true};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -121,43 +96,33 @@ export function closeEditTypeModal() {
|
|||||||
return {type: ActionTypes.CLOSE_EDIT_TYPE_MODAL};
|
return {type: ActionTypes.CLOSE_EDIT_TYPE_MODAL};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openAddSubscription() {
|
export function openGrantModal(data?: GrantModalData) {
|
||||||
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
return {type: ActionTypes.OPEN_GRANT_MODAL, data: data || {}};
|
||||||
const command = '/badges subscription create';
|
}
|
||||||
clientExecuteCommand(dispatch, getState, command);
|
|
||||||
|
|
||||||
|
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};
|
return {data: true};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openRemoveSubscription() {
|
export function openRemoveSubscription() {
|
||||||
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
return (dispatch: Dispatch<AnyAction>) => {
|
||||||
const command = '/badges subscription remove';
|
dispatch(openSubscriptionModal({mode: 'delete'}));
|
||||||
clientExecuteCommand(dispatch, getState, command);
|
|
||||||
|
|
||||||
return {data: true};
|
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 {ClientError} from 'mattermost-redux/client/client4';
|
||||||
|
|
||||||
import manifest from 'manifest';
|
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 {
|
export default class Client {
|
||||||
private url: string;
|
private url: string;
|
||||||
@ -74,6 +74,27 @@ export default class Client {
|
|||||||
await this.doDelete(`${this.url}/deleteType/${typeID}`);
|
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} = {}) => {
|
private doGet = async (url: string, headers: {[x:string]: string} = {}) => {
|
||||||
headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset());
|
headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset());
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
.BadgeModal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -16,6 +50,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
animation: badgeModalBackdropIn 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__dialog {
|
&__dialog {
|
||||||
@ -30,6 +65,27 @@
|
|||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
&__header {
|
||||||
@ -66,6 +122,12 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grant-intro {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@ -84,6 +146,11 @@
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
opacity: 0.64;
|
opacity: 0.64;
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: var(--error-text, #d24b4e);
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> input[type='text'],
|
> input[type='text'],
|
||||||
|
|||||||
@ -60,6 +60,7 @@ const BadgeModal: React.FC = () => {
|
|||||||
const [confirmDeleteTypeId, setConfirmDeleteTypeId] = useState<string | null>(null);
|
const [confirmDeleteTypeId, setConfirmDeleteTypeId] = useState<string | null>(null);
|
||||||
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false);
|
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false);
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
|
const [closing, setClosing] = useState(false);
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
const dialogRef = useRef<HTMLDivElement>(null);
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -114,15 +115,21 @@ const BadgeModal: React.FC = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [isOpen, isEditMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [isOpen, isEditMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const doClose = useCallback(() => {
|
||||||
if (createVisible) {
|
if (createVisible) {
|
||||||
dispatch(closeCreateBadgeModal());
|
dispatch(closeCreateBadgeModal());
|
||||||
}
|
}
|
||||||
if (editData) {
|
if (editData) {
|
||||||
dispatch(closeEditBadgeModal());
|
dispatch(closeEditBadgeModal());
|
||||||
}
|
}
|
||||||
|
setClosing(false);
|
||||||
}, [dispatch, createVisible, editData]);
|
}, [dispatch, createVisible, editData]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setClosing(true);
|
||||||
|
setTimeout(doClose, 150);
|
||||||
|
}, [doClose]);
|
||||||
|
|
||||||
const handleTypeSelect = useCallback((val: string) => {
|
const handleTypeSelect = useCallback((val: string) => {
|
||||||
if (val === NEW_TYPE_VALUE) {
|
if (val === NEW_TYPE_VALUE) {
|
||||||
setShowCreateType(true);
|
setShowCreateType(true);
|
||||||
@ -240,7 +247,7 @@ const BadgeModal: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [editData, confirmDelete, handleClose, intl, dispatch]);
|
}, [editData, confirmDelete, handleClose, intl, dispatch]);
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen && !closing) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +260,7 @@ const BadgeModal: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='BadgeModal'
|
className={'BadgeModal' + (closing ? ' BadgeModal--closing' : '')}
|
||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -280,6 +287,7 @@ const BadgeModal: React.FC = () => {
|
|||||||
id='badges.modal.field_name'
|
id='badges.modal.field_name'
|
||||||
defaultMessage='Название'
|
defaultMessage='Название'
|
||||||
/>
|
/>
|
||||||
|
<span className='required'>{'*'}</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
@ -309,6 +317,7 @@ const BadgeModal: React.FC = () => {
|
|||||||
id='badges.modal.field_image'
|
id='badges.modal.field_image'
|
||||||
defaultMessage='Эмодзи'
|
defaultMessage='Эмодзи'
|
||||||
/>
|
/>
|
||||||
|
<span className='required'>{'*'}</span>
|
||||||
</label>
|
</label>
|
||||||
<div className='emoji-input'>
|
<div className='emoji-input'>
|
||||||
<button
|
<button
|
||||||
@ -351,6 +360,7 @@ const BadgeModal: React.FC = () => {
|
|||||||
id='badges.modal.field_type'
|
id='badges.modal.field_type'
|
||||||
defaultMessage='Тип'
|
defaultMessage='Тип'
|
||||||
/>
|
/>
|
||||||
|
<span className='required'>{'*'}</span>
|
||||||
</label>
|
</label>
|
||||||
<TypeSelect
|
<TypeSelect
|
||||||
types={types}
|
types={types}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const InlineTypeForm: React.FC<Props> = ({form, onChange}) => {
|
|||||||
id='badges.modal.new_type_name'
|
id='badges.modal.new_type_name'
|
||||||
defaultMessage='Название типа'
|
defaultMessage='Название типа'
|
||||||
/>
|
/>
|
||||||
|
<span className='required'>{'*'}</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
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);
|
||||||
|
}, [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 [canEditType, setCanEditType] = useState(false);
|
||||||
const [canCreateType, setCanCreateType] = useState(false);
|
const [canCreateType, setCanCreateType] = useState(false);
|
||||||
|
const [canCreateBadge, setCanCreateBadge] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
client.getTypes().then((resp) => {
|
client.getTypes().then((resp) => {
|
||||||
setCanEditType(resp.can_edit_type);
|
setCanEditType(resp.can_edit_type);
|
||||||
setCanCreateType(resp.can_create_type);
|
setCanCreateType(resp.can_create_type);
|
||||||
|
setCanCreateBadge(resp.types.length > 0 || resp.can_create_type);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -89,7 +91,7 @@ const RHS: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{currentView === RHS_STATE_ALL && (
|
{currentView === RHS_STATE_ALL && canCreateBadge && (
|
||||||
<button
|
<button
|
||||||
className='AllBadges__createButton'
|
className='AllBadges__createButton'
|
||||||
onClick={handleCreateBadge}
|
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 [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
|
const [closing, setClosing] = useState(false);
|
||||||
|
|
||||||
const updateForm = useCallback((updates: Partial<TypeFormData>) => {
|
const updateForm = useCallback((updates: Partial<TypeFormData>) => {
|
||||||
setForm((prev) => ({...prev, ...updates}));
|
setForm((prev) => ({...prev, ...updates}));
|
||||||
@ -57,15 +58,21 @@ const TypeModal: React.FC = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [isOpen, isEditMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [isOpen, isEditMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const doClose = useCallback(() => {
|
||||||
if (createVisible) {
|
if (createVisible) {
|
||||||
dispatch(closeCreateTypeModal());
|
dispatch(closeCreateTypeModal());
|
||||||
}
|
}
|
||||||
if (editData) {
|
if (editData) {
|
||||||
dispatch(closeEditTypeModal());
|
dispatch(closeEditTypeModal());
|
||||||
}
|
}
|
||||||
|
setClosing(false);
|
||||||
}, [dispatch, createVisible, editData]);
|
}, [dispatch, createVisible, editData]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setClosing(true);
|
||||||
|
setTimeout(doClose, 150);
|
||||||
|
}, [doClose]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(async () => {
|
const handleSubmit = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@ -112,7 +119,7 @@ const TypeModal: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [editData, confirmDelete, handleClose, intl]);
|
}, [editData, confirmDelete, handleClose, intl]);
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen && !closing) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +131,7 @@ const TypeModal: React.FC = () => {
|
|||||||
: intl.formatMessage({id: 'badges.modal.btn_create', defaultMessage: 'Создать'});
|
: intl.formatMessage({id: 'badges.modal.btn_create', defaultMessage: 'Создать'});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='BadgeModal'>
|
<div className={'BadgeModal' + (closing ? ' BadgeModal--closing' : '')}>
|
||||||
<div
|
<div
|
||||||
className='BadgeModal__backdrop'
|
className='BadgeModal__backdrop'
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
@ -146,6 +153,7 @@ const TypeModal: React.FC = () => {
|
|||||||
id='badges.modal.field_name'
|
id='badges.modal.field_name'
|
||||||
defaultMessage='Название'
|
defaultMessage='Название'
|
||||||
/>
|
/>
|
||||||
|
<span className='required'>{'*'}</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
@ -184,7 +192,7 @@ const TypeModal: React.FC = () => {
|
|||||||
<span className='form-group__help'>
|
<span className='form-group__help'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='badges.modal.allowlist_create_help'
|
id='badges.modal.allowlist_create_help'
|
||||||
defaultMessage='Пользователи, кото<EFBFBD><EFBFBD>ые могут создавать значки этого типа.'
|
defaultMessage='Пользователи, которые могут создавать значки этого типа.'
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,4 +22,6 @@ export const initialState: PluginState = {
|
|||||||
editBadgeModalData: null,
|
editBadgeModalData: null,
|
||||||
createTypeModalVisible: false,
|
createTypeModalVisible: false,
|
||||||
editTypeModalData: null,
|
editTypeModalData: null,
|
||||||
|
grantModalData: null,
|
||||||
|
subscriptionModalData: null,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,6 +19,8 @@ import {openAddSubscription, openCreateBadge, openCreateType, openRemoveSubscrip
|
|||||||
import RHSComponent from 'components/rhs';
|
import RHSComponent from 'components/rhs';
|
||||||
import BadgeModal from 'components/badge_modal';
|
import BadgeModal from 'components/badge_modal';
|
||||||
import TypeModal from 'components/type_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';
|
import ChannelHeaderButton from 'components/channel_header_button';
|
||||||
|
|
||||||
@ -64,6 +66,8 @@ export default class Plugin {
|
|||||||
|
|
||||||
registry.registerRootComponent(withIntl(BadgeModal));
|
registry.registerRootComponent(withIntl(BadgeModal));
|
||||||
registry.registerRootComponent(withIntl(TypeModal));
|
registry.registerRootComponent(withIntl(TypeModal));
|
||||||
|
registry.registerRootComponent(withIntl(GrantModal));
|
||||||
|
registry.registerRootComponent(withIntl(SubscriptionModal));
|
||||||
|
|
||||||
const locale = getCurrentUser(store.getState())?.locale || 'ru';
|
const locale = getCurrentUser(store.getState())?.locale || 'ru';
|
||||||
const messages = getTranslations(locale);
|
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({
|
export default combineReducers({
|
||||||
showRHS,
|
showRHS,
|
||||||
rhsView,
|
rhsView,
|
||||||
@ -114,4 +136,6 @@ export default combineReducers({
|
|||||||
editBadgeModalData,
|
editBadgeModalData,
|
||||||
createTypeModalVisible,
|
createTypeModalVisible,
|
||||||
editTypeModalData,
|
editTypeModalData,
|
||||||
|
grantModalData,
|
||||||
|
subscriptionModalData,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -85,3 +85,17 @@ export const getEditTypeModalData = createSelector(
|
|||||||
return state.editTypeModalData;
|
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_create: string;
|
||||||
allowlist_can_grant: 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 RHSState = string;
|
||||||
|
|
||||||
|
export type GrantModalData = {
|
||||||
|
prefillUser?: string;
|
||||||
|
prefillBadgeId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubscriptionModalData = {
|
||||||
|
mode: 'create' | 'delete';
|
||||||
|
}
|
||||||
|
|
||||||
export type PluginState = {
|
export type PluginState = {
|
||||||
showRHS: (() => void)| null;
|
showRHS: (() => void)| null;
|
||||||
rhsView: RHSState;
|
rhsView: RHSState;
|
||||||
@ -13,4 +22,6 @@ export type PluginState = {
|
|||||||
editBadgeModalData: BadgeDetails | null;
|
editBadgeModalData: BadgeDetails | null;
|
||||||
createTypeModalVisible: boolean;
|
createTypeModalVisible: boolean;
|
||||||
editTypeModalData: BadgeTypeDefinition | null;
|
editTypeModalData: BadgeTypeDefinition | null;
|
||||||
|
grantModalData: GrantModalData | null;
|
||||||
|
subscriptionModalData: SubscriptionModalData | null;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user