@blocklet/ui-react
Version:
Some useful front-end web components that can be used in Blocklets.
268 lines (241 loc) • 7.22 kB
JSX
/* eslint-disable @typescript-eslint/naming-convention */
/**
* OrgMutateDialog - 组织创建/编辑弹窗
*
* 支持两种模式:
* - 创建模式:新建组织,头像必填
* - 编辑模式:编辑已有组织信息
*/
import { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useReactive, useMemoizedFn } from 'ahooks';
import noop from 'lodash/noop';
import Dialog from '@arcblock/ux/lib/Dialog';
import { CircularProgress, DialogContentText, Typography, TextField, Alert, Box } from '@mui/material';
import Toast from '@arcblock/ux/lib/Toast';
import Button from '@arcblock/ux/lib/Button';
import { translate } from '@arcblock/ux/lib/Locale/util';
import { formatAxiosError } from '../../UserCenter/libs/utils';
import useOrg from './use-org';
import translations from './locales';
import AvatarUploader from './avatar-uploader';
// 操作模式
const MODE_CREATE = 'create';
const MODE_EDIT = 'edit';
export default function OrgMutateDialog({
mode = MODE_CREATE,
org = null,
onSuccess = noop,
onCancel = noop,
locale = 'en',
teamDid: teamDidProp = '',
prefix = '/.well-known/service',
headers = noop,
}) {
const isEditMode = mode === MODE_EDIT;
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const { createOrg, updateOrg } = useOrg();
// 获取 teamDid:优先使用 prop,否则从 window.blocklet.did 获取
// eslint-disable-next-line no-undef
const teamDid = teamDidProp || (typeof window !== 'undefined' ? window.blocklet?.did : '');
const t = useMemoizedFn((key, data = {}) => {
return translate(translations, key, locale, 'en', data);
});
const form = useReactive({
name: '',
description: '',
avatar: '',
});
// 编辑模式下初始化表单数据
useEffect(() => {
if (isEditMode && org) {
form.name = org.name || '';
form.description = org.description || '';
form.avatar = org.avatar || '';
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isEditMode, org]);
// 头像变化处理
const handleAvatarChange = useMemoizedFn((avatarPath) => {
setError('');
form.avatar = avatarPath;
});
// 表单验证
const validateForm = useMemoizedFn(() => {
const _name = form.name.trim();
if (!_name) {
setError(t('nameEmpty'));
return false;
}
if (_name.length > 25) {
setError(t('nameTooLong', { length: 25 }));
return false;
}
const _description = form.description.trim();
if (_description.length > 255) {
setError(t('descriptionTooLong', { length: 255 }));
return false;
}
// 头像必填验证
if (!form.avatar) {
setError(t('avatarEmpty'));
return false;
}
return true;
});
// 提交处理
const onSubmit = async () => {
if (!validateForm()) {
return;
}
setError('');
setLoading(true);
const _name = form.name.trim();
const _description = form.description.trim();
const _avatar = form.avatar;
try {
if (isEditMode && org?.id) {
// 编辑模式:更新组织
await updateOrg(org.id, {
name: _name,
description: _description,
avatar: _avatar,
});
} else {
// 创建模式:新建组织
await createOrg({
name: _name,
description: _description,
avatar: _avatar,
});
}
onSuccess();
} catch (err) {
console.error(err);
const errMsg = formatAxiosError(err);
setError(errMsg);
Toast.error(errMsg);
} finally {
setLoading(false);
}
};
// 对话框标题
const dialogTitle = isEditMode ? t('mutate.title', { mode: t('edit') }) : t('mutate.title', { mode: t('create') });
// 提交按钮文本
const submitButtonText = isEditMode ? t('save') : t('create');
return (
<Dialog
title={dialogTitle}
fullWidth
open
onClose={onCancel}
showCloseButton={false}
actions={
<>
<Button onClick={onCancel} color="inherit">
{t('cancel')}
</Button>
<Button
data-cy="mutate-org-confirm"
onClick={onSubmit}
color="primary"
disabled={loading}
variant="contained"
autoFocus>
{loading && <CircularProgress size={16} sx={{ mr: 1 }} />}
{submitButtonText}
</Button>
</>
}>
<DialogContentText component="div">
{/* 头像上传区域 */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
py: 2,
}}>
<AvatarUploader
org={isEditMode ? org : { name: form.name }}
size={90}
teamDid={teamDid}
prefix={prefix}
headers={headers}
onChange={handleAvatarChange}
value={form.avatar}
editable
/>
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
{t('avatar')}
</Typography>
</Box>
{/* 表单字段 */}
<Typography component="div" style={{ marginTop: 16 }}>
<TextField
label={t('mutate.name')}
autoComplete="off"
variant="outlined"
name="name"
data-cy="mutate-org-input-name"
fullWidth
autoFocus
value={form.name}
onChange={(e) => {
setError('');
form.name = e.target.value;
}}
disabled={loading}
required
/>
</Typography>
<Typography component="div" style={{ marginTop: 16, marginBottom: 16 }}>
<TextField
label={t('mutate.description')}
autoComplete="off"
variant="outlined"
name="description"
data-cy="mutate-org-input-description"
fullWidth
value={form.description}
onChange={(e) => {
setError('');
form.description = e.target.value;
}}
disabled={loading}
multiline
rows={3}
/>
</Typography>
</DialogContentText>
{!!error && (
<Alert severity="error" style={{ width: '100%', margin: 0 }}>
{error}
</Alert>
)}
</Dialog>
);
}
OrgMutateDialog.propTypes = {
/** 操作模式:'create' 创建 | 'edit' 编辑 */
mode: PropTypes.oneOf([MODE_CREATE, MODE_EDIT]),
/** 编辑模式下的组织数据 */
org: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
description: PropTypes.string,
avatar: PropTypes.string,
}),
onSuccess: PropTypes.func,
onCancel: PropTypes.func,
locale: PropTypes.string,
teamDid: PropTypes.string,
prefix: PropTypes.string,
headers: PropTypes.func,
};
// 导出模式常量供外部使用
OrgMutateDialog.MODE_CREATE = MODE_CREATE;
OrgMutateDialog.MODE_EDIT = MODE_EDIT;
// 保持向后兼容的别名
export { OrgMutateDialog as CreateOrgDialog };