From 0d582ec80390b4ecc61c814309b569b5373deb84 Mon Sep 17 00:00:00 2001 From: "dmitrii.pichenikin" Date: Tue, 3 Mar 2026 11:07:43 +0300 Subject: [PATCH] replace interactive dialogs with custom modals for grant and subscription flows --- server/api.go | 225 ++++++++++++++ webapp/i18n/en.json | 28 ++ webapp/i18n/ru.json | 28 ++ webapp/src/action_types/index.ts | 4 + webapp/src/actions/actions.ts | 85 ++--- webapp/src/client/api.ts | 23 +- .../components/badge_modal/badge_modal.scss | 67 ++++ webapp/src/components/badge_modal/index.tsx | 16 +- .../badge_modal/inline_type_form.tsx | 1 + webapp/src/components/grant_modal/index.tsx | 292 ++++++++++++++++++ webapp/src/components/rhs/index.tsx | 4 +- .../components/subscription_modal/index.tsx | 206 ++++++++++++ webapp/src/components/type_modal/index.tsx | 16 +- webapp/src/constants.ts | 2 + webapp/src/index.tsx | 4 + webapp/src/reducers/index.ts | 24 ++ webapp/src/selectors/index.ts | 14 + webapp/src/types/badges.ts | 13 + webapp/src/types/general.ts | 11 + 19 files changed, 994 insertions(+), 69 deletions(-) create mode 100644 webapp/src/components/grant_modal/index.tsx create mode 100644 webapp/src/components/subscription_modal/index.tsx diff --git a/server/api.go b/server/api.go index 69ed300..6e3afaa 100644 --- a/server/api.go +++ b/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) +} diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index dc70000..17592a0 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -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", diff --git a/webapp/i18n/ru.json b/webapp/i18n/ru.json index cbd0404..059416e 100644 --- a/webapp/i18n/ru.json +++ b/webapp/i18n/ru.json @@ -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": "Не удалось загрузить типы", diff --git a/webapp/src/action_types/index.ts b/webapp/src/action_types/index.ts index eaa067e..d12e25f 100644 --- a/webapp/src/action_types/index.ts +++ b/webapp/src/action_types/index.ts @@ -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', }; diff --git a/webapp/src/actions/actions.ts b/webapp/src/actions/actions.ts index c873be8..a0cb756 100644 --- a/webapp/src/actions/actions.ts +++ b/webapp/src/actions/actions.ts @@ -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, getState: GetStateFunc) => { - let command = '/badges grant'; - if (user) { - command += ` --user ${user}`; - } - - if (badge) { - command += ` --badge ${badge}`; - } - - clientExecuteCommand(dispatch, getState, command); - + return (dispatch: Dispatch) => { + dispatch(openGrantModal({prefillUser: user, prefillBadgeId: badge})); return {data: true}; }; } export function openCreateType() { - return (dispatch: Dispatch, getState: GetStateFunc) => { - const command = '/badges create type'; - clientExecuteCommand(dispatch, getState, command); - + return (dispatch: Dispatch) => { + 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, 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) => { + dispatch(openSubscriptionModal({mode: 'create'})); return {data: true}; }; } export function openRemoveSubscription() { - return (dispatch: Dispatch, getState: GetStateFunc) => { - const command = '/badges subscription remove'; - clientExecuteCommand(dispatch, getState, command); - + return (dispatch: Dispatch) => { + dispatch(openSubscriptionModal({mode: 'delete'})); return {data: true}; }; } -export async function clientExecuteCommand(dispatch: Dispatch, 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 - } -} diff --git a/webapp/src/client/api.ts b/webapp/src/client/api.ts index 87d0259..66c02fc 100644 --- a/webapp/src/client/api.ts +++ b/webapp/src/client/api.ts @@ -5,7 +5,7 @@ import {Client4} from 'mattermost-redux/client'; import {ClientError} from 'mattermost-redux/client/client4'; import manifest from 'manifest'; -import {AllBadgesBadge, Badge, BadgeDetails, BadgeID, BadgeTypeDefinition, CreateBadgeRequest, CreateTypeRequest, GetTypesResponse, 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 { + await this.doPost(`${this.url}/grantBadge`, req); + } + + async createSubscription(req: SubscriptionRequest): Promise { + await this.doPost(`${this.url}/createSubscription`, req); + } + + async deleteSubscription(req: SubscriptionRequest): Promise { + await this.doPost(`${this.url}/deleteSubscription`, req); + } + + async getChannelSubscriptions(channelID: string): Promise { + 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()); diff --git a/webapp/src/components/badge_modal/badge_modal.scss b/webapp/src/components/badge_modal/badge_modal.scss index ffb5ac4..9b9304a 100644 --- a/webapp/src/components/badge_modal/badge_modal.scss +++ b/webapp/src/components/badge_modal/badge_modal.scss @@ -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'], diff --git a/webapp/src/components/badge_modal/index.tsx b/webapp/src/components/badge_modal/index.tsx index 270e045..2a21703 100644 --- a/webapp/src/components/badge_modal/index.tsx +++ b/webapp/src/components/badge_modal/index.tsx @@ -60,6 +60,7 @@ const BadgeModal: React.FC = () => { const [confirmDeleteTypeId, setConfirmDeleteTypeId] = useState(null); const [typeDropdownOpen, setTypeDropdownOpen] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false); + const [closing, setClosing] = useState(false); const modalRef = useRef(null); const dialogRef = useRef(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 (
{ id='badges.modal.field_name' defaultMessage='Название' /> + {'*'} { id='badges.modal.field_image' defaultMessage='Эмодзи' /> + {'*'}
+
+
+ {hasFixedUser && form.userDisplayName && ( +

+ +

+ )} +
+ +
+ + {badgeDropdownOpen && ( +
+ {badges.length === 0 && ( +
+ +
+ )} + {badges.map((badge) => ( +
handleBadgeSelect(String(badge.id))} + > + + + {' '}{badge.name} + + {badge.type_name} +
+ ))} +
+ )} +
+
+
+ +