470 lines
21 KiB
TypeScript
470 lines
21 KiB
TypeScript
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;
|