Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center

This commit is contained in:
LinkinStars 2023-04-20 17:52:25 +08:00
commit 045cd0b1e8
24 changed files with 197 additions and 142 deletions

View File

@ -1183,6 +1183,7 @@ ui:
plugins: plugins:
login: Login login: Login
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
oauth: oauth:
connect: Connect with {{ auth_name }} connect: Connect with {{ auth_name }}
remove: Remove {{ auth_name }} remove: Remove {{ auth_name }}

View File

@ -8,6 +8,7 @@ import {
} from 'react'; } from 'react';
import { markdownToHtml } from '@/services'; import { markdownToHtml } from '@/services';
import ImgViewer from '@/components/ImgViewer';
import { htmlRender } from './utils'; import { htmlRender } from './utils';
@ -48,11 +49,13 @@ const Index = ({ value }, ref) => {
}); });
return ( return (
<div <ImgViewer>
ref={previewRef} <div
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt" ref={previewRef}
dangerouslySetInnerHTML={{ __html: html }} className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
/> dangerouslySetInnerHTML={{ __html: html }}
/>
</ImgViewer>
); );
}; };

View File

@ -0,0 +1,7 @@
.img-viewer .cursor-zoom-out {
cursor: zoom-out !important;
}
.img-viewer img:not(a img, img.broken) {
cursor: zoom-in;
}

View File

@ -1,14 +1,13 @@
import { useLayoutEffect, useState, MouseEvent, useEffect } from 'react'; import { FC, MouseEvent, ReactNode, useEffect, useState } from 'react';
import { Modal } from 'react-bootstrap'; import { Modal } from 'react-bootstrap';
import { useLocation } from 'react-router-dom';
import ReactDOM from 'react-dom/client'; import './index.css';
import classnames from 'classnames';
const div = document.createElement('div'); const Index: FC<{
const root = ReactDOM.createRoot(div); children: ReactNode;
className?: classnames.Argument;
const useImgViewer = () => { }> = ({ children, className }) => {
const location = useLocation();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [imgSrc, setImgSrc] = useState(''); const [imgSrc, setImgSrc] = useState('');
const onClose = () => { const onClose = () => {
@ -47,8 +46,18 @@ const useImgViewer = () => {
} }
}; };
useLayoutEffect(() => { useEffect(() => {
root.render( return () => {
onClose();
};
}, []);
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div
className={classnames('img-viewer', className)}
onClick={checkClickForImgView}>
{children}
<Modal <Modal
show={visible} show={visible}
fullscreen fullscreen
@ -56,23 +65,16 @@ const useImgViewer = () => {
scrollable scrollable
contentClassName="bg-transparent" contentClassName="bg-transparent"
onHide={onClose}> onHide={onClose}>
<Modal.Body onClick={onClose} className="p-0 d-flex"> <Modal.Body onClick={onClose} className="img-viewer p-0 d-flex">
<img <img
className="cursor-zoom-out img-fluid m-auto" className="cursor-zoom-out img-fluid m-auto"
src={imgSrc} src={imgSrc}
alt={imgSrc} alt={imgSrc}
/> />
</Modal.Body> </Modal.Body>
</Modal>, </Modal>
); </div>
}); );
useEffect(() => {
onClose();
}, [location]);
return {
onClose,
checkClickForImgView,
};
}; };
export default useImgViewer; export default Index;

View File

@ -1,5 +1,5 @@
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { Button } from 'react-bootstrap'; import { Button, ButtonProps } from 'react-bootstrap';
import { request } from '@/utils'; import { request } from '@/utils';
import type * as Type from '@/common/interface'; import type * as Type from '@/common/interface';
@ -11,14 +11,18 @@ interface Props {
action: UIAction | undefined; action: UIAction | undefined;
formData: Type.FormDataType; formData: Type.FormDataType;
readOnly: boolean; readOnly: boolean;
variant?: ButtonProps['variant'];
size?: ButtonProps['size'];
} }
const Index: FC<Props> = ({ const Index: FC<Props> = ({
fieldName, fieldName,
action, action,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
formData, formData,
readOnly = false,
text = '', text = '',
readOnly = false,
variant = 'primary',
size,
}) => { }) => {
const [isLoading, setLoading] = useState(false); const [isLoading, setLoading] = useState(false);
const handleAction = async () => { const handleAction = async () => {
@ -33,7 +37,12 @@ const Index: FC<Props> = ({
const disabled = isLoading || readOnly; const disabled = isLoading || readOnly;
return ( return (
<div className="d-flex"> <div className="d-flex">
<Button name={fieldName} onClick={handleAction} disabled={disabled}> <Button
name={fieldName}
onClick={handleAction}
disabled={disabled}
size={size}
variant={variant}>
{text || fieldName} {text || fieldName}
{isLoading ? '...' : ''} {isLoading ? '...' : ''}
</Button> </Button>

View File

@ -42,7 +42,7 @@ const Index: FC<Props> = ({
type={type} type={type}
value={fieldObject?.value || ''} value={fieldObject?.value || ''}
onChange={handleChange} onChange={handleChange}
readOnly={readOnly} disabled={readOnly}
isInvalid={fieldObject?.isInvalid} isInvalid={fieldObject?.isInvalid}
style={type === 'color' ? { width: '6rem' } : {}} style={type === 'color' ? { width: '6rem' } : {}}
/> />

View File

@ -10,6 +10,7 @@ interface Props {
enumValues: (string | boolean | number)[]; enumValues: (string | boolean | number)[];
enumNames: string[]; enumNames: string[];
formData: Type.FormDataType; formData: Type.FormDataType;
readOnly: boolean;
} }
const Index: FC<Props> = ({ const Index: FC<Props> = ({
desc, desc,
@ -18,6 +19,7 @@ const Index: FC<Props> = ({
enumValues, enumValues,
enumNames, enumNames,
formData, formData,
readOnly = false,
}) => { }) => {
const fieldObject = formData[fieldName]; const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLSelectElement>) => { const handleChange = (evt: React.ChangeEvent<HTMLSelectElement>) => {
@ -40,6 +42,7 @@ const Index: FC<Props> = ({
name={fieldName} name={fieldName}
value={fieldObject?.value || ''} value={fieldObject?.value || ''}
onChange={handleChange} onChange={handleChange}
disabled={readOnly}
isInvalid={fieldObject?.isInvalid}> isInvalid={fieldObject?.isInvalid}>
{enumValues?.map((item, index) => { {enumValues?.map((item, index) => {
return ( return (

View File

@ -12,6 +12,7 @@ interface Props {
fieldName: string; fieldName: string;
onChange?: (fd: Type.FormDataType) => void; onChange?: (fd: Type.FormDataType) => void;
formData: Type.FormDataType; formData: Type.FormDataType;
readOnly: boolean;
} }
const Index: FC<Props> = ({ const Index: FC<Props> = ({
placeholder = '', placeholder = '',
@ -20,6 +21,7 @@ const Index: FC<Props> = ({
fieldName, fieldName,
onChange, onChange,
formData, formData,
readOnly = false,
}) => { }) => {
const fieldObject = formData[fieldName]; const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
@ -46,6 +48,7 @@ const Index: FC<Props> = ({
onChange={handleChange} onChange={handleChange}
isInvalid={fieldObject?.isInvalid} isInvalid={fieldObject?.isInvalid}
rows={rows} rows={rows}
disabled={readOnly}
className={classnames(className)} className={classnames(className)}
/> />
); );

View File

@ -4,7 +4,7 @@ import React, {
useImperativeHandle, useImperativeHandle,
useEffect, useEffect,
} from 'react'; } from 'react';
import { Form, Button } from 'react-bootstrap'; import { Form, Button, ButtonProps } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import classnames from 'classnames'; import classnames from 'classnames';
@ -100,6 +100,8 @@ export interface ButtonOptions extends BaseUIOptions {
text: string; text: string;
icon?: string; icon?: string;
action?: UIAction; action?: UIAction;
variant?: ButtonProps['variant'];
size?: ButtonProps['size'];
} }
export type UIOptions = export type UIOptions =
@ -367,6 +369,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
enumValues={enumValues} enumValues={enumValues}
enumNames={enumNames} enumNames={enumNames}
formData={formData} formData={formData}
readOnly={readOnly}
/> />
) : null} ) : null}
{widget === 'radio' || widget === 'checkbox' ? ( {widget === 'radio' || widget === 'checkbox' ? (
@ -418,6 +421,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
fieldName={key} fieldName={key}
onChange={onChange} onChange={onChange}
formData={formData} formData={formData}
readOnly={readOnly}
/> />
) : null} ) : null}
{widget === 'input' ? ( {widget === 'input' ? (
@ -439,6 +443,10 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
action={uiOpt && 'action' in uiOpt ? uiOpt.action : undefined} action={uiOpt && 'action' in uiOpt ? uiOpt.action : undefined}
formData={formData} formData={formData}
readOnly={readOnly} readOnly={readOnly}
variant={
uiOpt && 'variant' in uiOpt ? uiOpt.variant : undefined
}
size={uiOpt && 'size' in uiOpt ? uiOpt.size : undefined}
/> />
) : null} ) : null}
{/* Unified handling of `Feedback` and `Text` */} {/* Unified handling of `Feedback` and `Text` */}

View File

@ -1,12 +1,14 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
import { uploadImage } from '@/services'; import { uploadImage } from '@/services';
import * as Type from '@/common/interface'; import * as Type from '@/common/interface';
interface IProps { interface IProps {
type: Type.UploadType; type: Type.UploadType;
className?: string; className?: classnames.Argument;
children?: React.ReactNode; children?: React.ReactNode;
acceptType?: string; acceptType?: string;
disabled?: boolean; disabled?: boolean;
@ -49,7 +51,8 @@ const Index: React.FC<IProps> = ({
}; };
return ( return (
<label className={`btn btn-outline-secondary uploadBtn ${className}`}> <label
className={classnames('btn btn-outline-secondary uploadBtn', className)}>
{children || (status ? t('upload_img.loading') : t('upload_img.name'))} {children || (status ? t('upload_img.loading') : t('upload_img.name'))}
<input <input
type="file" type="file"

View File

@ -39,6 +39,7 @@ import QuestionList from './QuestionList';
import HotQuestions from './HotQuestions'; import HotQuestions from './HotQuestions';
import HttpErrorContent from './HttpErrorContent'; import HttpErrorContent from './HttpErrorContent';
import CustomSidebar from './CustomSidebar'; import CustomSidebar from './CustomSidebar';
import ImgViewer from './ImgViewer';
export { export {
Avatar, Avatar,
@ -84,5 +85,6 @@ export {
HotQuestions, HotQuestions,
HttpErrorContent, HttpErrorContent,
CustomSidebar, CustomSidebar,
ImgViewer,
}; };
export type { EditorRef, JSONSchema, UISchema }; export type { EditorRef, JSONSchema, UISchema };

View File

@ -10,7 +10,6 @@ import useChangePasswordModal from './useChangePasswordModal';
import usePageTags from './usePageTags'; import usePageTags from './usePageTags';
import useLoginRedirect from './useLoginRedirect'; import useLoginRedirect from './useLoginRedirect';
import usePromptWithUnload from './usePrompt'; import usePromptWithUnload from './usePrompt';
import useImgViewer from './useImgViewer';
export { export {
useTagModal, useTagModal,
@ -25,5 +24,4 @@ export {
usePageTags, usePageTags,
useLoginRedirect, useLoginRedirect,
usePromptWithUnload, usePromptWithUnload,
useImgViewer,
}; };

View File

@ -122,14 +122,6 @@ a {
cursor: pointer; cursor: pointer;
} }
.cursor-zoom-out {
cursor: zoom-out !important;
}
img:not(a img, img.broken) {
cursor: zoom-in;
}
.resize-none { .resize-none {
resize: none; resize: none;
} }

View File

@ -1,7 +1,7 @@
import { FC, memo, useEffect, useState } from 'react'; import { FC, memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { JSONSchema, SchemaForm, UISchema } from '@/components'; import { JSONSchema, SchemaForm, UISchema, ImgViewer } from '@/components';
import { FormDataType } from '@/common/interface'; import { FormDataType } from '@/common/interface';
import { brandSetting, getBrandSetting } from '@/services'; import { brandSetting, getBrandSetting } from '@/services';
import { brandingStore } from '@/stores'; import { brandingStore } from '@/stores';
@ -142,7 +142,7 @@ const Index: FC = () => {
}, []); }, []);
return ( return (
<div> <ImgViewer>
<h3 className="mb-4">{t('page_title')}</h3> <h3 className="mb-4">{t('page_title')}</h3>
<SchemaForm <SchemaForm
schema={schema} schema={schema}
@ -151,7 +151,7 @@ const Index: FC = () => {
onSubmit={onSubmit} onSubmit={onSubmit}
onChange={handleOnChange} onChange={handleOnChange}
/> />
</div> </ImgViewer>
); );
}; };

View File

@ -146,6 +146,9 @@ const Index: FC = () => {
useEffect(() => { useEffect(() => {
getUsersSetting().then((resp) => { getUsersSetting().then((resp) => {
if (!resp) {
return;
}
const formMeta: Type.FormDataType = {}; const formMeta: Type.FormDataType = {};
Object.keys(formData).forEach((k) => { Object.keys(formData).forEach((k) => {
let v = resp[k]; let v = resp[k];

View File

@ -15,7 +15,6 @@ import {
HttpErrorContent, HttpErrorContent,
} from '@/components'; } from '@/components';
import { LoginToContinueModal } from '@/components/Modal'; import { LoginToContinueModal } from '@/components/Modal';
import { useImgViewer } from '@/hooks';
const Layout: FC = () => { const Layout: FC = () => {
const location = useLocation(); const location = useLocation();
@ -24,7 +23,6 @@ const Layout: FC = () => {
toastClear(); toastClear();
}; };
const { code: httpStatusCode, reset: httpStatusReset } = errorCodeStore(); const { code: httpStatusCode, reset: httpStatusReset } = errorCodeStore();
const imgViewer = useImgViewer();
const { show: showLoginToContinueModal } = loginToContinueStore(); const { show: showLoginToContinueModal } = loginToContinueStore();
useEffect(() => { useEffect(() => {
@ -40,9 +38,7 @@ const Layout: FC = () => {
}}> }}>
<Header /> <Header />
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
<div <div className="position-relative page-wrap">
className="position-relative page-wrap"
onClick={imgViewer.checkClickForImgView}>
{httpStatusCode ? ( {httpStatusCode ? (
<HttpErrorContent httpCode={httpStatusCode} /> <HttpErrorContent httpCode={httpStatusCode} />
) : ( ) : (

View File

@ -12,6 +12,7 @@ import {
FormatTime, FormatTime,
htmlRender, htmlRender,
Icon, Icon,
ImgViewer,
} from '@/components'; } from '@/components';
import { formatCount, guard } from '@/utils'; import { formatCount, guard } from '@/utils';
import { following } from '@/services'; import { following } from '@/services';
@ -114,11 +115,13 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
return <Tag className="m-1" key={item.slug_name} data={item} />; return <Tag className="m-1" key={item.slug_name} data={item} />;
})} })}
</div> </div>
<article <ImgViewer>
ref={ref} <article
className="fmt text-break text-wrap mt-4" ref={ref}
dangerouslySetInnerHTML={{ __html: data?.html }} className="fmt text-break text-wrap mt-4"
/> dangerouslySetInnerHTML={{ __html: data?.html }}
/>
</ImgViewer>
<Actions <Actions
className="mt-4" className="mt-4"

View File

@ -35,7 +35,7 @@ const Index: FC = () => {
if (!targetUrl) { if (!targetUrl) {
return; return;
} }
QrCode.toDataURL(targetUrl, { width: 240 }, (err, url) => { QrCode.toDataURL(targetUrl, { width: 240, margin: 0 }, (err, url) => {
if (err) { if (err) {
return; return;
} }
@ -67,14 +67,21 @@ const Index: FC = () => {
return ( return (
<Card className="text-center"> <Card className="text-center">
<Card.Body> <Card.Body>
<Card.Title as="h3"> <Card.Title as="h3" className="mb-3">
{agentName} {t('login')} {ucAgent?.agent_info.display_name} {t('login')}
</Card.Title> </Card.Title>
{qrcodeDataUrl ? ( {qrcodeDataUrl ? (
<> <>
<img width={240} height={240} src={qrcodeDataUrl} alt="" /> <img
<div className="text-secondary"> width={240}
{t('qrcode_login_tip', { agentName })} height={240}
src={qrcodeDataUrl}
alt={agentName}
/>
<div className="text-secondary mt-3">
{t('qrcode_login_tip', {
agentName: ucAgent?.agent_info.display_name,
})}
</div> </div>
</> </>
) : null} ) : null}

View File

@ -1,5 +1,8 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { Card, Col, Carousel } from 'react-bootstrap'; import { Card, Col, Carousel } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { userCenterStore } from '@/stores';
const data = [ const data = [
{ {
@ -25,14 +28,17 @@ const data = [
]; ];
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'plugins' });
const ucAgent = userCenterStore().agent;
return ( return (
<Col lg={4} className="mx-auto mt-3 py-5"> <Col lg={4} className="mx-auto mt-3 py-5">
<Card> <Card>
<Card.Body> <Card.Body>
<h3 className="text-center pt-3 mb-3">WeCom Login</h3> <h3 className="text-center pt-3 mb-3">
{ucAgent?.agent_info.display_name} {t('login')}
</h3>
<p className="text-danger text-center"> <p className="text-danger text-center">
Login failed, please allow this app to access your email information {t('login_failed_email_tip')}
before try again.
</p> </p>
<Carousel controls={false}> <Carousel controls={false}>

View File

@ -16,7 +16,7 @@ import {
loginSettingStore, loginSettingStore,
userCenterStore, userCenterStore,
} from '@/stores'; } from '@/stores';
import { guard, handleFormError } from '@/utils'; import { floppyNavigation, guard, handleFormError, userCenter } from '@/utils';
import { login, checkImgCode, UcAgent } from '@/services'; import { login, checkImgCode, UcAgent } from '@/services';
import { PicAuthCodeModal } from '@/components/Modal'; import { PicAuthCodeModal } from '@/components/Modal';
@ -246,7 +246,10 @@ const Index: React.FC = () => {
<div className="text-center mt-5"> <div className="text-center mt-5">
<Trans i18nKey="login.info_sign" ns="translation"> <Trans i18nKey="login.info_sign" ns="translation">
Dont have an account? Dont have an account?
<Link to="/users/register" tabIndex={2}> <Link
to={userCenter.getSignUpUrl()}
tabIndex={2}
onClick={floppyNavigation.handleRouteLinkClick}>
Sign up Sign up
</Link> </Link>
</Trans> </Trans>

View File

@ -5,7 +5,7 @@ import { Trans, useTranslation } from 'react-i18next';
import MD5 from 'md5'; import MD5 from 'md5';
import type { FormDataType } from '@/common/interface'; import type { FormDataType } from '@/common/interface';
import { UploadImg, Avatar, Icon } from '@/components'; import { UploadImg, Avatar, Icon, ImgViewer } from '@/components';
import { loggedUserInfoStore, userCenterStore, siteInfoStore } from '@/stores'; import { loggedUserInfoStore, userCenterStore, siteInfoStore } from '@/stores';
import { useToast } from '@/hooks'; import { useToast } from '@/hooks';
import { import {
@ -27,7 +27,6 @@ const Index: React.FC = () => {
const [mailHash, setMailHash] = useState(''); const [mailHash, setMailHash] = useState('');
const [count] = useState(0); const [count] = useState(0);
const [profileAgent, setProfileAgent] = useState<UcSettingAgent>(); const [profileAgent, setProfileAgent] = useState<UcSettingAgent>();
const [formData, setFormData] = useState<FormDataType>({ const [formData, setFormData] = useState<FormDataType>({
display_name: { display_name: {
value: '', value: '',
@ -282,7 +281,7 @@ const Index: React.FC = () => {
<Form.Control <Form.Control
required required
type="text" type="text"
readOnly={!usersSetting.allow_update_display_name} disabled={!usersSetting.allow_update_display_name}
value={formData.display_name.value} value={formData.display_name.value}
isInvalid={formData.display_name.isInvalid} isInvalid={formData.display_name.isInvalid}
onChange={(e) => onChange={(e) =>
@ -305,7 +304,7 @@ const Index: React.FC = () => {
<Form.Control <Form.Control
required required
type="text" type="text"
readOnly={!usersSetting.allow_update_username} disabled={!usersSetting.allow_update_username}
value={formData.username.value} value={formData.username.value}
isInvalid={formData.username.isInvalid} isInvalid={formData.username.isInvalid}
onChange={(e) => onChange={(e) =>
@ -343,66 +342,68 @@ const Index: React.FC = () => {
</option> </option>
</Form.Select> </Form.Select>
</div> </div>
<div className="d-flex"> <ImgViewer>
{formData.avatar.type === 'gravatar' && ( <div className="d-flex">
<Stack> {formData.avatar.type === 'gravatar' && (
<Avatar <Stack>
size="160px"
avatar={formData.avatar.gravatar}
searchStr={`s=256&d=identicon${
count > 0 ? `&t=${new Date().valueOf()}` : ''
}`}
className="me-3 rounded"
/>
<Form.Text className="text-muted mt-1">
<Trans i18nKey="settings.profile.avatar.gravatar_text">
You can change image on
<a
href="https://gravatar.com"
target="_blank"
rel="noreferrer">
gravatar.com
</a>
</Trans>
</Form.Text>
</Stack>
)}
{formData.avatar.type === 'custom' && (
<Stack>
<Stack direction="horizontal" className="align-items-start">
<Avatar <Avatar
size="160px" size="160px"
searchStr="s=256" avatar={formData.avatar.gravatar}
avatar={formData.avatar.custom} searchStr={`s=256&d=identicon${
className="me-2 bg-gray-300 " count > 0 ? `&t=${new Date().valueOf()}` : ''
}`}
className="me-3 rounded"
/> />
<ButtonGroup vertical className="fit-content"> <Form.Text className="text-muted mt-1">
<UploadImg <Trans i18nKey="settings.profile.avatar.gravatar_text">
type="avatar" You can change image on
disabled={!usersSetting.allow_update_avatar} <a
uploadCallback={avatarUpload}> href="https://gravatar.com"
<Icon name="cloud-upload" /> target="_blank"
</UploadImg> rel="noreferrer">
<Button gravatar.com
variant="outline-secondary" </a>
disabled={!usersSetting.allow_update_avatar} </Trans>
onClick={removeCustomAvatar}> </Form.Text>
<Icon name="trash" />
</Button>
</ButtonGroup>
</Stack> </Stack>
<Form.Text className="text-muted mt-1"> )}
<Trans i18nKey="settings.profile.avatar.text">
You can upload your image. {formData.avatar.type === 'custom' && (
</Trans> <Stack>
</Form.Text> <Stack direction="horizontal" className="align-items-start">
</Stack> <Avatar
)} size="160px"
{formData.avatar.type === 'default' && ( searchStr="s=256"
<Avatar size="160px" avatar="" /> avatar={formData.avatar.custom}
)} className="me-2 bg-gray-300 "
</div> />
<ButtonGroup vertical className="fit-content">
<UploadImg
type="avatar"
disabled={!usersSetting.allow_update_avatar}
uploadCallback={avatarUpload}>
<Icon name="cloud-upload" />
</UploadImg>
<Button
variant="outline-secondary"
disabled={!usersSetting.allow_update_avatar}
onClick={removeCustomAvatar}>
<Icon name="trash" />
</Button>
</ButtonGroup>
</Stack>
<Form.Text className="text-muted mt-1">
<Trans i18nKey="settings.profile.avatar.text">
You can upload your image.
</Trans>
</Form.Text>
</Stack>
)}
{formData.avatar.type === 'default' && (
<Avatar size="160px" avatar="" />
)}
</div>
</ImgViewer>
<Form.Control <Form.Control
isInvalid={formData.avatar.isInvalid} isInvalid={formData.avatar.isInvalid}
className="d-none" className="d-none"
@ -423,7 +424,7 @@ const Index: React.FC = () => {
required required
as="textarea" as="textarea"
rows={5} rows={5}
readOnly={!usersSetting.allow_update_bio} disabled={!usersSetting.allow_update_bio}
value={formData.bio.value} value={formData.bio.value}
isInvalid={formData.bio.isInvalid} isInvalid={formData.bio.isInvalid}
onChange={(e) => onChange={(e) =>
@ -449,7 +450,7 @@ const Index: React.FC = () => {
required required
type="url" type="url"
placeholder={t('website.placeholder')} placeholder={t('website.placeholder')}
readOnly={!usersSetting.allow_update_website} disabled={!usersSetting.allow_update_website}
value={formData.website.value} value={formData.website.value}
isInvalid={formData.website.isInvalid} isInvalid={formData.website.isInvalid}
onChange={(e) => onChange={(e) =>
@ -475,7 +476,7 @@ const Index: React.FC = () => {
required required
type="text" type="text"
placeholder={t('location.placeholder')} placeholder={t('location.placeholder')}
readOnly={!usersSetting.allow_update_location} disabled={!usersSetting.allow_update_location}
value={formData.location.value} value={formData.location.value}
isInvalid={formData.location.isInvalid} isInvalid={formData.location.isInvalid}
onChange={(e) => onChange={(e) =>

View File

@ -13,7 +13,6 @@ interface Props {
const Index: FC<Props> = ({ className }) => { const Index: FC<Props> = ({ className }) => {
const { t } = useTranslation('translation', { keyPrefix: 'plugins.oauth' }); const { t } = useTranslation('translation', { keyPrefix: 'plugins.oauth' });
const ucAgent = userCenterStore().agent; const ucAgent = userCenterStore().agent;
const agentName = ucAgent?.agent_info?.name || '';
const ucLoginRedirect = const ucLoginRedirect =
ucAgent?.enabled && ucAgent?.agent_info?.login_redirect_url; ucAgent?.enabled && ucAgent?.agent_info?.login_redirect_url;
@ -24,7 +23,9 @@ const Index: FC<Props> = ({ className }) => {
variant="outline-secondary" variant="outline-secondary"
href={ucAgent?.agent_info.login_redirect_url}> href={ucAgent?.agent_info.login_redirect_url}>
<SvgIcon base64={ucAgent?.agent_info.icon} /> <SvgIcon base64={ucAgent?.agent_info.icon} />
<span>{t('connect', { auth_name: agentName })}</span> <span>
{t('connect', { auth_name: ucAgent?.agent_info.display_name })}
</span>
</Button> </Button>
); );
} }

View File

@ -11,6 +11,7 @@ export interface UcAgent {
name: string; name: string;
icon: string; icon: string;
url: string; url: string;
display_name: string;
login_redirect_url: string; login_redirect_url: string;
sign_up_redirect_url: string; sign_up_redirect_url: string;
control_center: UcAgentControl[]; control_center: UcAgentControl[];

View File

@ -13,6 +13,16 @@ interface SiteInfoType {
updateUsers: (users: SiteInfoType['users']) => void; updateUsers: (users: SiteInfoType['users']) => void;
} }
const defaultUsersConf: AdminSettingsUsers = {
allow_update_avatar: false,
allow_update_bio: false,
allow_update_display_name: false,
allow_update_location: false,
allow_update_username: false,
allow_update_website: false,
default_avatar: 'system',
};
const siteInfo = create<SiteInfoType>((set) => ({ const siteInfo = create<SiteInfoType>((set) => ({
siteInfo: { siteInfo: {
name: DEFAULT_SITE_NAME, name: DEFAULT_SITE_NAME,
@ -22,15 +32,7 @@ const siteInfo = create<SiteInfoType>((set) => ({
contact_email: '', contact_email: '',
permalink: 1, permalink: 1,
}, },
users: { users: defaultUsersConf,
allow_update_avatar: false,
allow_update_bio: false,
allow_update_display_name: false,
allow_update_location: false,
allow_update_username: false,
allow_update_website: false,
default_avatar: 'system',
},
version: '', version: '',
revision: '', revision: '',
update: (params) => update: (params) =>
@ -50,6 +52,7 @@ const siteInfo = create<SiteInfoType>((set) => ({
}, },
updateUsers: (users) => { updateUsers: (users) => {
set(() => { set(() => {
users ||= defaultUsersConf;
return { users }; return { users };
}); });
}, },