470 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 {isCreateBadgeModalVisible, getEditBadgeModalData} from 'selectors';
import {closeCreateBadgeModal, closeEditBadgeModal} from 'actions/actions';
import {BadgeTypeDefinition} from 'types/badges';
import Client from 'client/api';
import {getServerErrorId} from 'utils/helpers';
import CloseIcon from 'components/icons/close_icon';
import EmojiIcon from 'components/icons/emoji_icon';
import RenderEmoji from 'components/utils/emoji';
import EmojiPickerOverlay from './emoji_picker';
import TypeSelect from './type_select';
import './badge_modal.scss';
const NEW_TYPE_VALUE = '__new__';
const BadgeModal: React.FC = () => {
const dispatch = useDispatch();
const intl = useIntl();
const createVisible = useSelector(isCreateBadgeModalVisible);
const editData = useSelector(getEditBadgeModalData);
const channelId = useSelector((state: GlobalState) => getCurrentChannelId(state));
const isOpen = createVisible || editData !== null;
const isEditMode = editData !== null;
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [image, setImage] = useState('');
const [badgeType, setBadgeType] = useState('');
const [multiple, setMultiple] = useState(false);
const [types, setTypes] = useState<BadgeTypeDefinition[]>([]);
const [showCreateType, setShowCreateType] = useState(false);
const [newTypeName, setNewTypeName] = useState('');
const [newTypeEveryoneCanCreate, setNewTypeEveryoneCanCreate] = useState(false);
const [newTypeEveryoneCanGrant, setNewTypeEveryoneCanGrant] = useState(false);
const [canCreateType, setCanCreateType] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [confirmDelete, setConfirmDelete] = useState(false);
const [confirmDeleteTypeId, setConfirmDeleteTypeId] = useState<string | null>(null);
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const dialogRef = useRef<HTMLDivElement>(null);
const emojiData = (window as any)?.useGetEmojiSelectorData?.();
const {
emojiButtonRef,
calculateRightOffSet,
} = emojiData || {};
useEffect(() => {
if (!isOpen) {
return;
}
const fetchTypes = async () => {
const client = new Client();
const resp = await client.getTypes();
setTypes(resp.types);
setCanCreateType(resp.can_create_type);
if (!isEditMode && resp.types.length > 0) {
const defaultType = resp.types.find((t) => t.is_default);
setBadgeType(String(defaultType ? defaultType.id : resp.types[0].id));
}
};
fetchTypes();
if (isEditMode && editData) {
setName(editData.name);
setDescription(editData.description);
setImage(editData.image);
setBadgeType(String(editData.type));
setMultiple(editData.multiple);
} else {
setName('');
setDescription('');
setImage('');
setBadgeType('');
setMultiple(false);
}
setShowCreateType(false);
setNewTypeName('');
setNewTypeEveryoneCanCreate(false);
setNewTypeEveryoneCanGrant(false);
setError(null);
setConfirmDelete(false);
setConfirmDeleteTypeId(null);
setTypeDropdownOpen(false);
setShowEmojiPicker(false);
setLoading(false);
}, [isOpen, isEditMode]); // eslint-disable-line react-hooks/exhaustive-deps
const handleClose = useCallback(() => {
if (createVisible) {
dispatch(closeCreateBadgeModal());
}
if (editData) {
dispatch(closeEditBadgeModal());
}
}, [dispatch, createVisible, editData]);
const handleTypeSelect = useCallback((val: string) => {
if (val === NEW_TYPE_VALUE) {
setShowCreateType(true);
setBadgeType('');
} else {
setShowCreateType(false);
setBadgeType(val);
}
setTypeDropdownOpen(false);
setConfirmDeleteTypeId(null);
}, []);
const handleEmojiSelect = (emoji: any) => {
if (emoji.short_name) {
setImage(emoji.short_name);
} else if (emoji.name) {
setImage(emoji.name);
}
setShowEmojiPicker(false);
};
const handleDeleteType = useCallback(async (typeId: string) => {
if (confirmDeleteTypeId !== typeId) {
setConfirmDeleteTypeId(typeId);
return;
}
try {
const client = new Client();
await client.deleteType(typeId);
const removeById = (t: BadgeTypeDefinition) => String(t.id) !== typeId;
setTypes((prev) => prev.filter(removeById));
if (badgeType === typeId) {
setBadgeType('');
}
} catch (err) {
setError(intl.formatMessage({id: 'badges.error.' + (getServerErrorId(err) || 'unknown'), defaultMessage: 'Произошла ошибка'}));
}
setConfirmDeleteTypeId(null);
}, [confirmDeleteTypeId, badgeType, intl]);
const handleSubmit = useCallback(async () => {
setLoading(true);
setError(null);
try {
const client = new Client();
let typeID = badgeType;
if (showCreateType) {
if (!newTypeName.trim()) {
setError(intl.formatMessage({id: 'badges.modal.error_type_name_required', defaultMessage: 'Введите название типа'}));
setLoading(false);
return;
}
const createdType = await client.createType({
name: newTypeName.trim(),
everyone_can_create: newTypeEveryoneCanCreate,
everyone_can_grant: newTypeEveryoneCanGrant,
channel_id: channelId,
});
typeID = String(createdType.id);
}
if (!typeID) {
setError(intl.formatMessage({id: 'badges.modal.error_type_required', defaultMessage: 'Выберите тип значка'}));
setLoading(false);
return;
}
if (isEditMode && editData) {
await client.updateBadge({
id: String(editData.id),
name: name.trim(),
description: description.trim(),
image: image.trim(),
type: typeID,
multiple,
});
} else {
await client.createBadge({
name: name.trim(),
description: description.trim(),
image: image.trim(),
type: typeID,
multiple,
channel_id: channelId,
});
}
handleClose();
} catch (err) {
setError(intl.formatMessage({id: 'badges.error.' + (getServerErrorId(err) || 'unknown'), defaultMessage: 'Произошла ошибка'}));
} finally {
setLoading(false);
}
}, [badgeType, showCreateType, newTypeName, newTypeEveryoneCanCreate, newTypeEveryoneCanGrant, isEditMode, editData, name, description, image, multiple, handleClose, intl, channelId]);
const handleDelete = useCallback(async () => {
if (!editData) {
return;
}
if (!confirmDelete) {
setConfirmDelete(true);
return;
}
setLoading(true);
setError(null);
try {
const client = new Client();
await client.deleteBadge(editData.id);
handleClose();
} catch (err) {
setError(intl.formatMessage({id: 'badges.error.' + (getServerErrorId(err) || 'unknown'), defaultMessage: 'Произошла ошибка'}));
} finally {
setLoading(false);
}
}, [editData, confirmDelete, handleClose, intl]);
if (!isOpen) {
return null;
}
const title = isEditMode ? intl.formatMessage({id: 'badges.modal.edit_badge_title', defaultMessage: 'Редактировать значок'}) : intl.formatMessage({id: 'badges.modal.create_badge_title', defaultMessage: 'Создать значок'});
const submitLabel = isEditMode ? intl.formatMessage({id: 'badges.modal.btn_save', defaultMessage: 'Сохранить'}) : intl.formatMessage({id: 'badges.modal.btn_create', defaultMessage: 'Создать'});
return (
<div className='BadgeModal'>
<div
className='BadgeModal__backdrop'
onClick={handleClose}
/>
<div
className='BadgeModal__dialog'
ref={dialogRef}
>
<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.modal.field_name'
defaultMessage='Название'
/>
</label>
<input
type='text'
value={name}
onChange={(e) => setName(e.target.value)}
maxLength={20}
placeholder={intl.formatMessage({id: 'badges.modal.field_name_placeholder', defaultMessage: 'Название значка (макс. 20 символов)'})}
/>
</div>
<div className='form-group'>
<label>
<FormattedMessage
id='badges.modal.field_description'
defaultMessage='Описание'
/>
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
maxLength={120}
placeholder={intl.formatMessage({id: 'badges.modal.field_description_placeholder', defaultMessage: 'Описание значка (макс. 120 символов)'})}
/>
</div>
<div className='form-group'>
<label>
<FormattedMessage
id='badges.modal.field_image'
defaultMessage='Эмодзи'
/>
</label>
<div className='emoji-input'>
<button
type='button'
className='emoji-input__icon'
onClick={() => setShowEmojiPicker((prev) => !prev)}
ref={emojiButtonRef}
>
<EmojiIcon/>
</button>
{image && (
<RenderEmoji
emojiName={image}
size={20}
/>
)}
<input
type='text'
value={image}
onChange={(e) => setImage(e.target.value)}
placeholder={intl.formatMessage({id: 'badges.modal.field_image_placeholder', defaultMessage: 'Название эмодзи (напр. star)'})}
/>
</div>
{showEmojiPicker && (
<EmojiPickerOverlay
target={() => emojiButtonRef?.current}
container={() => dialogRef.current}
show={showEmojiPicker}
onHide={() => setShowEmojiPicker(false)}
onEmojiClick={handleEmojiSelect}
rightOffset={calculateRightOffSet?.(emojiButtonRef?.current)}
defaultHorizontalPosition='right'
hideCustomEmojiButton={true}
/>
)}
</div>
<div className='form-group'>
<label>
<FormattedMessage
id='badges.modal.field_type'
defaultMessage='Тип'
/>
</label>
<TypeSelect
types={types}
badgeType={badgeType}
showCreateType={showCreateType}
canCreateType={canCreateType}
typeDropdownOpen={typeDropdownOpen}
confirmDeleteTypeId={confirmDeleteTypeId}
onToggleDropdown={() => setTypeDropdownOpen(!typeDropdownOpen)}
onSelect={handleTypeSelect}
onDeleteType={handleDeleteType}
onCancelDeleteType={() => setConfirmDeleteTypeId(null)}
/>
{showCreateType && (
<div className='inline-type-section'>
<div className='form-group'>
<label>
<FormattedMessage
id='badges.modal.new_type_name'
defaultMessage='Название типа'
/>
</label>
<input
type='text'
value={newTypeName}
onChange={(e) => setNewTypeName(e.target.value)}
maxLength={20}
placeholder={intl.formatMessage({id: 'badges.modal.new_type_name_placeholder', defaultMessage: 'Название типа (макс. 20 символов)'})}
/>
</div>
<div className='checkbox-group'>
<input
type='checkbox'
id='newTypeEveryoneCanCreate'
checked={newTypeEveryoneCanCreate}
onChange={(e) => setNewTypeEveryoneCanCreate(e.target.checked)}
/>
<label htmlFor='newTypeEveryoneCanCreate'>
<FormattedMessage
id='badges.modal.new_type_everyone_create'
defaultMessage='Все могут создавать значки'
/>
</label>
</div>
<div className='checkbox-group'>
<input
type='checkbox'
id='newTypeEveryoneCanGrant'
checked={newTypeEveryoneCanGrant}
onChange={(e) => setNewTypeEveryoneCanGrant(e.target.checked)}
/>
<label htmlFor='newTypeEveryoneCanGrant'>
<FormattedMessage
id='badges.modal.new_type_everyone_grant'
defaultMessage='Все могут выдавать значки'
/>
</label>
</div>
</div>
)}
</div>
<div className='checkbox-group'>
<input
type='checkbox'
id='badgeMultiple'
checked={multiple}
onChange={(e) => setMultiple(e.target.checked)}
/>
<label htmlFor='badgeMultiple'>
<FormattedMessage
id='badges.modal.field_multiple'
defaultMessage='Можно выдавать несколько раз'
/>
</label>
</div>
{error && <div className='error-message'>{error}</div>}
{isEditMode && (
<div className='delete-section'>
{confirmDelete ? (
<div className='confirm-delete'>
<span>
<FormattedMessage
id='badges.modal.confirm_delete'
defaultMessage='Вы уверены?'
/>
</span>
<button
className='btn btn--danger'
onClick={handleDelete}
disabled={loading}
>
<FormattedMessage
id='badges.modal.btn_confirm_delete'
defaultMessage='Да, удалить'
/>
</button>
<button
className='btn btn--cancel'
onClick={() => setConfirmDelete(false)}
>
<FormattedMessage
id='badges.modal.btn_cancel'
defaultMessage='Отмена'
/>
</button>
</div>
) : (
<button
className='btn btn--danger'
onClick={handleDelete}
disabled={loading}
>
<FormattedMessage
id='badges.modal.btn_delete'
defaultMessage='Удалить значок'
/>
</button>
)}
</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 || !name.trim() || !image.trim()}
>
{loading ? intl.formatMessage({id: 'badges.modal.btn_creating', defaultMessage: 'Сохранение...'}) : submitLabel}
</button>
</div>
</div>
</div>
);
};
export default BadgeModal;