feat: refactor agreement modal and create folders to classify components (#1686)

* refactor: refactor agreement modal and create folders to classify components

* fix: i18

* fix: i18

* fix: i18n
This commit is contained in:
Yaodong Yu
2023-03-26 18:44:47 +08:00
committed by GitHub
parent 32b05047dc
commit a8937d3046
49 changed files with 628 additions and 488 deletions

View File

@ -0,0 +1,126 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Checkbox, Form, Modal} from "antd";
import i18next from "i18next";
import React, {useEffect, useState} from "react";
export const AgreementModal = (props) => {
const {open, onOk, onCancel, application} = props;
const [doc, setDoc] = useState("");
useEffect(() => {
getTermsOfUseContent(application.termsOfUseUrl).then((data) => {
setDoc(data);
});
}, []);
return (
<Modal
title={i18next.t("signup:Terms of Use")}
open={open}
width={"55vw"}
closable={false}
okText={i18next.t("signup:Accept")}
cancelText={i18next.t("signup:Decline")}
onOk={onOk}
onCancel={onCancel}
>
<iframe title={"terms"} style={{border: 0, width: "100%", height: "60vh"}} srcDoc={doc} />
</Modal>
);
};
function getTermsOfUseContent(url) {
return fetch(url, {
method: "GET",
}).then(r => r.text());
}
export function isAgreementRequired(application) {
if (application) {
const agreementItem = application.signupItems.find(item => item.name === "Agreement");
if (!agreementItem || agreementItem.rule === "None" || !agreementItem.rule) {
return false;
}
if (agreementItem.required) {
return true;
}
}
return false;
}
function initDefaultValue(application) {
const agreementItem = application.signupItems.find(item => item.name === "Agreement");
return isAgreementRequired(application) && agreementItem.rule === "Signin (Default True)";
}
export function renderAgreementFormItem(application, required, layout, ths) {
return (<React.Fragment>
<Form.Item
name="agreement"
key="agreement"
valuePropName="checked"
rules={[
{
required: required,
},
() => ({
validator: (_, value) => {
if (!required) {
return Promise.resolve();
}
if (!value) {
return Promise.reject(i18next.t("signup:Please accept the agreement!"));
} else {
return Promise.resolve();
}
},
}),
]
}
{...layout}
initialValue={initDefaultValue(application)}
>
<Checkbox style={{float: "left"}}>
{i18next.t("signup:Accept")}&nbsp;
<a onClick={() => {
ths.setState({
isTermsOfUseVisible: true,
});
}}
>
{i18next.t("signup:Terms of Use")}
</a>
</Checkbox>
</Form.Item>
<AgreementModal application={application} layout={layout} open={ths.state.isTermsOfUseVisible}
onOk={() => {
ths.form.current.setFieldsValue({agreement: true});
ths.setState({
isTermsOfUseVisible: false,
});
}}
onCancel={() => {
ths.form.current.setFieldsValue({agreement: false});
ths.setState({
isTermsOfUseVisible: false,
});
}} />
</React.Fragment>
);
}

View File

@ -0,0 +1,172 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Button, Col, Input, Modal, Row} from "antd";
import i18next from "i18next";
import React, {useEffect} from "react";
import * as UserBackend from "../../backend/UserBackend";
import {CaptchaWidget} from "../CaptchaWidget";
import {SafetyOutlined} from "@ant-design/icons";
export const CaptchaModal = (props) => {
const {owner, name, visible, onOk, onCancel, isCurrentProvider} = props;
const [captchaType, setCaptchaType] = React.useState("none");
const [clientId, setClientId] = React.useState("");
const [clientSecret, setClientSecret] = React.useState("");
const [subType, setSubType] = React.useState("");
const [clientId2, setClientId2] = React.useState("");
const [clientSecret2, setClientSecret2] = React.useState("");
const [open, setOpen] = React.useState(false);
const [captchaImg, setCaptchaImg] = React.useState("");
const [captchaToken, setCaptchaToken] = React.useState("");
const defaultInputRef = React.useRef(null);
useEffect(() => {
if (visible) {
loadCaptcha();
} else {
handleCancel();
setOpen(false);
}
}, [visible]);
const handleOk = () => {
onOk?.(captchaType, captchaToken, clientSecret);
};
const handleCancel = () => {
setCaptchaToken("");
onCancel?.();
};
const loadCaptcha = () => {
UserBackend.getCaptcha(owner, name, isCurrentProvider).then((res) => {
if (res.type === "none") {
handleOk();
} else if (res.type === "Default") {
setOpen(true);
setClientSecret(res.captchaId);
setCaptchaImg(res.captchaImage);
setCaptchaType("Default");
} else {
setOpen(true);
setCaptchaType(res.type);
setClientId(res.clientId);
setClientSecret(res.clientSecret);
setSubType(res.subType);
setClientId2(res.clientId2);
setClientSecret2(res.clientSecret2);
}
});
};
const renderDefaultCaptcha = () => {
return (
<Col style={{textAlign: "center"}}>
<div style={{display: "inline-block"}}>
<Row
style={{
backgroundImage: `url('data:image/png;base64,${captchaImg}')`,
backgroundRepeat: "no-repeat",
height: "80px",
width: "200px",
borderRadius: "5px",
border: "1px solid #ccc",
marginBottom: "20px",
}}
/>
<Row>
<Input
ref={defaultInputRef}
style={{width: "200px"}}
value={captchaToken}
prefix={<SafetyOutlined />}
placeholder={i18next.t("general:Captcha")}
onPressEnter={handleOk}
onChange={(e) => setCaptchaToken(e.target.value)}
/>
</Row>
</div>
</Col>
);
};
const onChange = (token) => {
setCaptchaToken(token);
};
const renderCaptcha = () => {
if (captchaType === "Default") {
return renderDefaultCaptcha();
} else {
return (
<Col>
<Row>
<CaptchaWidget
captchaType={captchaType}
subType={subType}
siteKey={clientId}
clientSecret={clientSecret}
onChange={onChange}
clientId2={clientId2}
clientSecret2={clientSecret2}
/>
</Row>
</Col>
);
}
};
const renderFooter = () => {
let isOkDisabled = false;
if (captchaType === "Default") {
const regex = /^\d{5}$/;
if (!regex.test(captchaToken)) {
isOkDisabled = true;
}
} else if (captchaToken === "") {
isOkDisabled = true;
}
return [
<Button key="cancel" onClick={handleCancel}>{i18next.t("general:Cancel")}</Button>,
<Button key="ok" disabled={isOkDisabled} type="primary" onClick={handleOk}>{i18next.t("general:OK")}</Button>,
];
};
return (
<Modal
closable={false}
maskClosable={false}
destroyOnClose={true}
title={i18next.t("general:Captcha")}
open={open}
okText={i18next.t("general:OK")}
cancelText={i18next.t("general:Cancel")}
width={350}
footer={renderFooter()}
onCancel={handleCancel}
onOk={handleOk}
>
<div style={{marginTop: "20px", marginBottom: "50px"}}>
{
renderCaptcha()
}
</div>
</Modal>
);
};

View File

@ -0,0 +1,190 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, {useEffect, useState} from "react";
import Cropper from "react-cropper";
import "cropperjs/dist/cropper.css";
import * as Setting from "../../Setting";
import {Button, Col, Modal, Row, Select} from "antd";
import i18next from "i18next";
import * as ResourceBackend from "../../backend/ResourceBackend";
export const CropperDivModal = (props) => {
const [loading, setLoading] = useState(true);
const [options, setOptions] = useState([]);
const [image, setImage] = useState("");
const [cropper, setCropper] = useState();
const [visible, setVisible] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const {title} = props;
const {user} = props;
const {buttonText} = props;
const {organization} = props;
let uploadButton;
const onChange = (e) => {
e.preventDefault();
let files;
if (e.dataTransfer) {
files = e.dataTransfer.files;
} else if (e.target) {
files = e.target.files;
}
const reader = new FileReader();
reader.onload = () => {
setImage(reader.result);
};
if (!(files[0] instanceof Blob)) {
return;
}
reader.readAsDataURL(files[0]);
};
const uploadAvatar = () => {
cropper.getCroppedCanvas().toBlob(blob => {
if (blob === null) {
Setting.showMessage("error", "You must select a picture first!");
return false;
}
// Setting.showMessage("success", "uploading...");
const extension = image.substring(image.indexOf("/") + 1, image.indexOf(";base64"));
const fullFilePath = `avatar/${user.owner}/${user.name}.${extension}`;
ResourceBackend.uploadResource(user.owner, user.name, "avatar", "CropperDivModal", fullFilePath, blob)
.then((res) => {
if (res.status === "ok") {
window.location.href = window.location.pathname;
} else {
Setting.showMessage("error", res.msg);
}
});
return true;
});
};
const showModal = () => {
setVisible(true);
};
const handleOk = () => {
setConfirmLoading(true);
if (!uploadAvatar()) {
setConfirmLoading(false);
}
};
const handleCancel = () => {
setVisible(false);
};
const selectFile = () => {
uploadButton.click();
};
const getOptions = (data) => {
const options = [];
options.push({value: organization?.defaultAvatar});
for (let i = 0; i < data.length; i++) {
if (data[i].fileType === "image") {
const url = `${data[i].url}`;
options.push({
value: url,
});
}
}
return options;
};
const getBase64Image = (src) => {
return new Promise((resolve) => {
const image = new Image();
image.src = src;
image.setAttribute("crossOrigin", "anonymous");
image.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, image.width, image.height);
const dataURL = canvas.toDataURL("image/png");
resolve(dataURL);
};
});
};
useEffect(() => {
setLoading(true);
ResourceBackend.getResources(user.owner, user.name, "", "", "", "", "", "")
.then((res) => {
setLoading(false);
setOptions(getOptions(res));
});
}, []);
return (
<div>
<Button type="default" onClick={showModal}>
{buttonText}
</Button>
<Modal
maskClosable={false}
title={title}
open={visible}
okText={i18next.t("user:Upload a photo")}
confirmLoading={confirmLoading}
onCancel={handleCancel}
width={600}
footer={
[<Button block key="submit" type="primary" onClick={handleOk}>{i18next.t("user:Set new profile picture")}</Button>]
}
>
<Col style={{margin: "0px auto 60px auto", width: 1000, height: 350}}>
<Row style={{width: "100%", marginBottom: "20px"}}>
<input style={{display: "none"}} ref={input => uploadButton = input} type="file" accept="image/*" onChange={onChange} />
<Button block onClick={selectFile}>{i18next.t("user:Select a photo...")}</Button>
<Select virtual={false}
style={{width: "100%"}}
loading={loading}
placeholder={i18next.t("user:Please select avatar from resources")}
onChange={(async value => {
setImage(await getBase64Image(value));
})}
options={options}
allowClear={true}
/>
</Row>
<Cropper
style={{height: "100%"}}
initialAspectRatio={1}
preview=".img-preview"
src={image}
viewMode={1}
guides={true}
minCropBoxHeight={10}
minCropBoxWidth={10}
background={false}
responsive={true}
autoCropArea={1}
checkOrientation={false}
onInitialized={(instance) => {
setCropper(instance);
}}
/>
</Col>
</Modal>
</div>
);
};
export default CropperDivModal;

View File

@ -0,0 +1,93 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Button, Col, Input, Modal, Row} from "antd";
import i18next from "i18next";
import React from "react";
import * as UserBackend from "../../backend/UserBackend";
import * as Setting from "../../Setting";
export const PasswordModal = (props) => {
const [visible, setVisible] = React.useState(false);
const [confirmLoading, setConfirmLoading] = React.useState(false);
const [oldPassword, setOldPassword] = React.useState("");
const [newPassword, setNewPassword] = React.useState("");
const [rePassword, setRePassword] = React.useState("");
const {user} = props;
const {account} = props;
const showModal = () => {
setVisible(true);
};
const handleCancel = () => {
setVisible(false);
};
const handleOk = () => {
if (newPassword === "" || rePassword === "") {
Setting.showMessage("error", i18next.t("user:Empty input!"));
return;
}
if (newPassword !== rePassword) {
Setting.showMessage("error", i18next.t("user:Two passwords you typed do not match."));
return;
}
setConfirmLoading(true);
UserBackend.setPassword(user.owner, user.name, oldPassword, newPassword).then((res) => {
setConfirmLoading(false);
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("user:Password set successfully"));
setVisible(false);
} else {Setting.showMessage("error", i18next.t(`user:${res.msg}`));}
});
};
const hasOldPassword = user.password !== "";
return (
<Row>
<Button type="default" disabled={props.disabled} onClick={showModal}>
{hasOldPassword ? i18next.t("user:Modify password...") : i18next.t("user:Set password...")}
</Button>
<Modal
maskClosable={false}
title={i18next.t("general:Password")}
open={visible}
okText={i18next.t("user:Set Password")}
cancelText={i18next.t("general:Cancel")}
confirmLoading={confirmLoading}
onCancel={handleCancel}
onOk={handleOk}
width={600}
>
<Col style={{margin: "0px auto 40px auto", width: 1000, height: 300}}>
{(hasOldPassword && !Setting.isAdminUser(account)) ? (
<Row style={{width: "100%", marginBottom: "20px"}}>
<Input.Password addonBefore={i18next.t("user:Old Password")} placeholder={i18next.t("user:input password")} onChange={(e) => setOldPassword(e.target.value)} />
</Row>
) : null}
<Row style={{width: "100%", marginBottom: "20px"}}>
<Input.Password addonBefore={i18next.t("user:New Password")} placeholder={i18next.t("user:input password")} onChange={(e) => setNewPassword(e.target.value)} />
</Row>
<Row style={{width: "100%", marginBottom: "20px"}}>
<Input.Password addonBefore={i18next.t("user:Re-enter New")} placeholder={i18next.t("user:input password")} onChange={(e) => setRePassword(e.target.value)} />
</Row>
</Col>
</Modal>
</Row>
);
};
export default PasswordModal;

View File

@ -0,0 +1,110 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Button, Col, Input, Modal, Row} from "antd";
import i18next from "i18next";
import React from "react";
import * as Setting from "../../Setting";
import * as UserBackend from "../../backend/UserBackend";
import {SendCodeInput} from "../SendCodeInput";
import {MailOutlined, PhoneOutlined} from "@ant-design/icons";
export const ResetModal = (props) => {
const [visible, setVisible] = React.useState(false);
const [confirmLoading, setConfirmLoading] = React.useState(false);
const [dest, setDest] = React.useState("");
const [code, setCode] = React.useState("");
const {buttonText, destType, application, countryCode} = props;
const showModal = () => {
setVisible(true);
};
const handleCancel = () => {
setVisible(false);
};
const handleOk = () => {
if (dest === "") {
if (destType === "phone") {
Setting.showMessage("error", i18next.t("user:Phone cannot be empty"));
} else {
Setting.showMessage("error", i18next.t("user:Email cannot be empty"));
}
return;
}
if (code === "") {
Setting.showMessage("error", i18next.t("code:Empty code"));
return;
}
setConfirmLoading(true);
UserBackend.resetEmailOrPhone(dest, destType, code).then(res => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("user:Email/phone reset successfully"));
window.location.reload();
} else {
Setting.showMessage("error", i18next.t("user:" + res.msg));
setConfirmLoading(false);
}
});
};
let placeholder = "";
if (destType === "email") {
placeholder = i18next.t("user:Input your email");
} else if (destType === "phone") {
placeholder = i18next.t("user:Input your phone number");
}
return (
<Row>
<Button type="default" onClick={showModal}>
{buttonText}
</Button>
<Modal
maskClosable={false}
title={buttonText}
open={visible}
okText={buttonText}
cancelText={i18next.t("general:Cancel")}
confirmLoading={confirmLoading}
onCancel={handleCancel}
onOk={handleOk}
width={600}
>
<Col style={{margin: "0px auto 40px auto", width: 1000, height: 300}}>
<Row style={{width: "100%", marginBottom: "20px"}}>
<Input
addonBefore={destType === "email" ? i18next.t("user:New Email") : i18next.t("user:New phone")}
prefix={destType === "email" ? <React.Fragment><MailOutlined />&nbsp;&nbsp;</React.Fragment> : (<React.Fragment><PhoneOutlined />&nbsp;&nbsp;{countryCode !== "" ? "+" : null}{Setting.getCountryCode(countryCode)}&nbsp;</React.Fragment>)}
placeholder={placeholder}
onChange={e => setDest(e.target.value)}
/>
</Row>
<Row style={{width: "100%", marginBottom: "20px"}}>
<SendCodeInput
textBefore={i18next.t("code:Code you received")}
onChange={setCode}
method={"reset"}
onButtonClickArgs={[dest, destType, Setting.getApplicationName(application)]}
application={application}
/>
</Row>
</Col>
</Modal>
</Row>
);
};
export default ResetModal;