mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-04 05:10:19 +08:00
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:
@ -15,7 +15,7 @@
|
||||
import {Button} from "antd";
|
||||
import React from "react";
|
||||
import i18next from "i18next";
|
||||
import {CaptchaModal} from "./CaptchaModal";
|
||||
import {CaptchaModal} from "./modal/CaptchaModal";
|
||||
import * as UserBackend from "../backend/UserBackend";
|
||||
|
||||
export const CaptchaPreview = (props) => {
|
||||
|
38
web/src/common/CustomGithubCorner.js
Normal file
38
web/src/common/CustomGithubCorner.js
Normal file
@ -0,0 +1,38 @@
|
||||
// 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 from "react";
|
||||
import * as Conf from "../Conf";
|
||||
import GithubCorner from "react-github-corner";
|
||||
|
||||
class CustomGithubCorner extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!Conf.ShowGithubCorner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GithubCorner href={Conf.GithubRepo} size={60} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomGithubCorner;
|
@ -1,335 +0,0 @@
|
||||
// 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 React from "react";
|
||||
import {DeleteOutlined, EditOutlined} from "@ant-design/icons";
|
||||
import {Button, Input, Popconfirm, Table, Tooltip} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import * as AdapterBackend from "../backend/AdapterBackend";
|
||||
import i18next from "i18next";
|
||||
|
||||
class PolicyTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
policyLists: [],
|
||||
loading: false,
|
||||
editingIndex: "",
|
||||
oldPolicy: "",
|
||||
add: false,
|
||||
page: 1,
|
||||
};
|
||||
}
|
||||
|
||||
count = 0;
|
||||
pageSize = 10;
|
||||
|
||||
getIndex(index) {
|
||||
// Need to be used in all place when modify table. Parameter is the row index in table, need to calculate the index in dataSource.
|
||||
return index + (this.state.page - 1) * 10;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
if (this.props.mode === "edit") {
|
||||
this.synPolicies();
|
||||
}
|
||||
}
|
||||
|
||||
isEditing = (index) => {
|
||||
return index === this.state.editingIndex;
|
||||
};
|
||||
|
||||
edit = (record, index) => {
|
||||
this.setState({editingIndex: index, oldPolicy: Setting.deepCopy(record)});
|
||||
};
|
||||
|
||||
cancel = (table, index) => {
|
||||
Object.keys(table[this.getIndex(index)]).forEach((key) => {
|
||||
table[this.getIndex(index)][key] = this.state.oldPolicy[key];
|
||||
});
|
||||
this.updateTable(table);
|
||||
this.setState({editingIndex: "", oldPolicy: ""});
|
||||
if (this.state.add) {
|
||||
this.deleteRow(this.state.policyLists, index);
|
||||
this.setState({add: false});
|
||||
}
|
||||
};
|
||||
|
||||
updateTable(table) {
|
||||
this.setState({policyLists: table});
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[this.getIndex(index)][key] = value;
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
addRow(table) {
|
||||
const row = {key: this.count, Ptype: "p"};
|
||||
if (table === undefined) {
|
||||
table = [];
|
||||
}
|
||||
table = Setting.addRow(table, row, "top");
|
||||
|
||||
this.count = this.count + 1;
|
||||
this.updateTable(table);
|
||||
this.edit(row, 0);
|
||||
this.setState({
|
||||
page: 1,
|
||||
add: true,
|
||||
});
|
||||
}
|
||||
|
||||
deleteRow(table, index) {
|
||||
table = Setting.deleteRow(table, this.getIndex(index));
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
save(table, i) {
|
||||
this.state.add ? this.addPolicy(table, i) : this.updatePolicy(table, i);
|
||||
}
|
||||
|
||||
synPolicies() {
|
||||
this.setState({loading: true});
|
||||
AdapterBackend.syncPolicies(this.props.owner, this.props.name)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("adapter:Sync policies successfully"));
|
||||
|
||||
const policyList = res.data;
|
||||
policyList.map((policy, index) => {
|
||||
policy.key = index;
|
||||
});
|
||||
this.count = policyList.length;
|
||||
this.setState({policyLists: policyList});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("adapter:Failed to sync policies")}: ${res.msg}`);
|
||||
}
|
||||
this.setState({loading: false});
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
updatePolicy(table, i) {
|
||||
AdapterBackend.UpdatePolicy(this.props.owner, this.props.name, [this.state.oldPolicy, table[i]]).then(res => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({editingIndex: "", oldPolicy: ""});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addPolicy(table, i) {
|
||||
AdapterBackend.AddPolicy(this.props.owner, this.props.name, table[i]).then(res => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({editingIndex: "", oldPolicy: "", add: false});
|
||||
if (res.data !== "Affected") {
|
||||
res.msg = i18next.t("adapter:Duplicated policy rules");
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
} else {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deletePolicy(table, index) {
|
||||
AdapterBackend.RemovePolicy(this.props.owner, this.props.name, table[this.getIndex(index)]).then(res => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
|
||||
this.deleteRow(table, index);
|
||||
} else {
|
||||
Setting.showMessage("error", i18next.t("general:Failed to delete"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const columns = [
|
||||
{
|
||||
title: "Rule Type",
|
||||
dataIndex: "Ptype",
|
||||
width: "100px",
|
||||
// render: (text, record, index) => {
|
||||
// const editing = this.isEditing(index);
|
||||
// return (
|
||||
// editing ?
|
||||
// <Input value={text} onChange={e => {
|
||||
// this.updateField(table, index, "Ptype", e.target.value);
|
||||
// }} />
|
||||
// : text
|
||||
// );
|
||||
// },
|
||||
},
|
||||
{
|
||||
title: "V0",
|
||||
dataIndex: "V0",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
const editing = this.isEditing(index);
|
||||
return (
|
||||
editing ?
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "V0", e.target.value);
|
||||
}} />
|
||||
: text
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "V1",
|
||||
dataIndex: "V1",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
const editing = this.isEditing(index);
|
||||
return (
|
||||
editing ?
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "V1", e.target.value);
|
||||
}} />
|
||||
: text
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "V2",
|
||||
dataIndex: "V2",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
const editing = this.isEditing(index);
|
||||
return (
|
||||
editing ?
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "V2", e.target.value);
|
||||
}} />
|
||||
: text
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "V3",
|
||||
dataIndex: "V3",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
const editing = this.isEditing(index);
|
||||
return (
|
||||
editing ?
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "V3", e.target.value);
|
||||
}} />
|
||||
: text
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "V4",
|
||||
dataIndex: "V4",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
const editing = this.isEditing(index);
|
||||
return (
|
||||
editing ?
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "V4", e.target.value);
|
||||
}} />
|
||||
: text
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "V5",
|
||||
dataIndex: "V5",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
const editing = this.isEditing(index);
|
||||
return (
|
||||
editing ?
|
||||
<Input value={text} onChange={e => {
|
||||
this.updateField(table, index, "V5", e.target.value);
|
||||
}} />
|
||||
: text
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Option",
|
||||
key: "option",
|
||||
width: "100px",
|
||||
render: (text, record, index) => {
|
||||
const editable = this.isEditing(index);
|
||||
return editable ? (
|
||||
<span>
|
||||
<Button style={{marginRight: 8}} onClick={() => this.save(table, index)}>
|
||||
Save
|
||||
</Button>
|
||||
<Popconfirm title="Sure to cancel?" onConfirm={() => this.cancel(table, index)}>
|
||||
<a>Cancel</a>
|
||||
</Popconfirm>
|
||||
</span>
|
||||
) : (
|
||||
<div>
|
||||
<Tooltip placement="topLeft" title="Edit">
|
||||
<Button disabled={this.state.editingIndex !== ""} style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => this.edit(record, index)} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="topLeft" title="Delete">
|
||||
<Button disabled={this.state.editingIndex !== ""} style={{marginRight: "5px"}} icon={<DeleteOutlined />} size="small" onClick={() => this.deletePolicy(table, index)} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}];
|
||||
|
||||
return (
|
||||
<Table
|
||||
pagination={{
|
||||
defaultPageSize: this.pageSize,
|
||||
onChange: (page) => this.setState({
|
||||
page: page,
|
||||
}),
|
||||
disabled: this.state.editingIndex !== "",
|
||||
current: this.state.page,
|
||||
}}
|
||||
columns={columns} dataSource={table} rowKey="key" size="middle" bordered
|
||||
loading={this.state.loading}
|
||||
title={() => (
|
||||
<div>
|
||||
<Button disabled={this.state.editingIndex !== ""} style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button type="primary" disabled={this.state.editingIndex !== ""} onClick={() => {this.synPolicies();}}>
|
||||
{i18next.t("general:Sync")}
|
||||
</Button>
|
||||
{
|
||||
this.renderTable(this.state.policyLists)
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PolicyTable;
|
@ -17,7 +17,7 @@ import React from "react";
|
||||
import i18next from "i18next";
|
||||
import * as UserBackend from "../backend/UserBackend";
|
||||
import {SafetyOutlined} from "@ant-design/icons";
|
||||
import {CaptchaModal} from "./CaptchaModal";
|
||||
import {CaptchaModal} from "./modal/CaptchaModal";
|
||||
|
||||
const {Search} = Input;
|
||||
|
||||
|
60
web/src/common/TestEmailWidget.js
Normal file
60
web/src/common/TestEmailWidget.js
Normal file
@ -0,0 +1,60 @@
|
||||
// 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 * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
export function sendTestEmail(provider, email) {
|
||||
testEmailProvider(provider, email)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", `${i18next.t("provider:Email sent successfully")}`);
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
export function connectSmtpServer(provider) {
|
||||
testEmailProvider(provider)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", "provider:SMTP connected successfully");
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
function testEmailProvider(provider, email = "") {
|
||||
const emailForm = {
|
||||
title: provider.title,
|
||||
content: provider.content,
|
||||
sender: provider.displayName,
|
||||
receivers: email === "" ? ["TestSmtpServer"] : [email],
|
||||
provider: provider.name,
|
||||
};
|
||||
|
||||
return fetch(`${Setting.ServerUrl}/api/send-email`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(emailForm),
|
||||
}).then(res => res.json());
|
||||
}
|
43
web/src/common/TestSmsWidget.js
Normal file
43
web/src/common/TestSmsWidget.js
Normal file
@ -0,0 +1,43 @@
|
||||
// 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 * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
export function sendTestSms(provider, phone) {
|
||||
testSmsProvider(provider, phone)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", `${i18next.t("provider:SMS sent successfully")}`);
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
function testSmsProvider(provider, phone = "") {
|
||||
const SmsForm = {
|
||||
content: "123456",
|
||||
receivers: [phone],
|
||||
};
|
||||
|
||||
return fetch(`${Setting.ServerUrl}/api/send-sms?provider=` + provider.name, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(SmsForm),
|
||||
}).then(res => res.json());
|
||||
}
|
126
web/src/common/modal/AgreementModal.js
Normal file
126
web/src/common/modal/AgreementModal.js
Normal 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")}
|
||||
<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>
|
||||
);
|
||||
}
|
@ -15,8 +15,8 @@
|
||||
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 * as UserBackend from "../../backend/UserBackend";
|
||||
import {CaptchaWidget} from "../CaptchaWidget";
|
||||
import {SafetyOutlined} from "@ant-design/icons";
|
||||
|
||||
export const CaptchaModal = (props) => {
|
190
web/src/common/modal/CropperDivModal.js
Normal file
190
web/src/common/modal/CropperDivModal.js
Normal 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;
|
93
web/src/common/modal/PasswordModal.js
Normal file
93
web/src/common/modal/PasswordModal.js
Normal 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;
|
110
web/src/common/modal/ResetModal.js
Normal file
110
web/src/common/modal/ResetModal.js
Normal 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 /> </React.Fragment> : (<React.Fragment><PhoneOutlined /> {countryCode !== "" ? "+" : null}{Setting.getCountryCode(countryCode)} </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;
|
@ -1,119 +1,119 @@
|
||||
// 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 from "react";
|
||||
import {Cascader, Col, Input, Row, Select} from "antd";
|
||||
import i18next from "i18next";
|
||||
import * as UserBackend from "../backend/UserBackend";
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
class AffiliationSelect extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
addressOptions: [],
|
||||
affiliationOptions: [],
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getAddressOptions(this.props.application);
|
||||
this.getAffiliationOptions(this.props.application, this.props.user);
|
||||
}
|
||||
|
||||
getAddressOptions(application) {
|
||||
if (application.affiliationUrl === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const addressUrl = application.affiliationUrl.split("|")[0];
|
||||
UserBackend.getAddressOptions(addressUrl)
|
||||
.then((addressOptions) => {
|
||||
this.setState({
|
||||
addressOptions: addressOptions,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAffiliationOptions(application, user) {
|
||||
if (application.affiliationUrl === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const affiliationUrl = application.affiliationUrl.split("|")[1];
|
||||
const code = user.address[user.address.length - 1];
|
||||
UserBackend.getAffiliationOptions(affiliationUrl, code)
|
||||
.then((affiliationOptions) => {
|
||||
this.setState({
|
||||
affiliationOptions: affiliationOptions,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateUserField(key, value) {
|
||||
this.props.onUpdateUserField(key, value);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{
|
||||
this.props.application?.affiliationUrl === "" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={this.props.labelSpan}>
|
||||
{Setting.getLabel(i18next.t("user:Address"), i18next.t("user:Address - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={24 - this.props.labelSpan} >
|
||||
<Cascader style={{width: "100%", maxWidth: "400px"}} value={this.props.user.address} options={this.state.addressOptions} onChange={value => {
|
||||
this.updateUserField("address", value);
|
||||
this.updateUserField("affiliation", "");
|
||||
this.updateUserField("score", 0);
|
||||
this.getAffiliationOptions(this.props.application, this.props.user);
|
||||
}} placeholder={i18next.t("signup:Please input your address!")} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={this.props.labelSpan}>
|
||||
{Setting.getLabel(i18next.t("user:Affiliation"), i18next.t("user:Affiliation - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{
|
||||
this.props.application?.affiliationUrl === "" ? (
|
||||
<Input value={this.props.user.affiliation} onChange={e => {
|
||||
this.updateUserField("affiliation", e.target.value);
|
||||
}} />
|
||||
) : (
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.props.user.affiliation}
|
||||
onChange={(value => {
|
||||
const name = value;
|
||||
const affiliationOption = Setting.getArrayItem(this.state.affiliationOptions, "name", name);
|
||||
const id = affiliationOption.id;
|
||||
this.updateUserField("affiliation", name);
|
||||
this.updateUserField("score", id);
|
||||
})}
|
||||
options={[Setting.getOption(`(${i18next.t("general:empty")})`, "")].concat(this.state.affiliationOptions.map((affiliationOption) => Setting.getOption(affiliationOption.name, affiliationOption.name))
|
||||
)} />
|
||||
)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AffiliationSelect;
|
||||
// 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 from "react";
|
||||
import {Cascader, Col, Input, Row, Select} from "antd";
|
||||
import i18next from "i18next";
|
||||
import * as UserBackend from "../../backend/UserBackend";
|
||||
import * as Setting from "../../Setting";
|
||||
|
||||
class AffiliationSelect extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
addressOptions: [],
|
||||
affiliationOptions: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getAddressOptions(this.props.application);
|
||||
this.getAffiliationOptions(this.props.application, this.props.user);
|
||||
}
|
||||
|
||||
getAddressOptions(application) {
|
||||
if (application.affiliationUrl === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const addressUrl = application.affiliationUrl.split("|")[0];
|
||||
UserBackend.getAddressOptions(addressUrl)
|
||||
.then((addressOptions) => {
|
||||
this.setState({
|
||||
addressOptions: addressOptions,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAffiliationOptions(application, user) {
|
||||
if (application.affiliationUrl === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const affiliationUrl = application.affiliationUrl.split("|")[1];
|
||||
const code = user.address[user.address.length - 1];
|
||||
UserBackend.getAffiliationOptions(affiliationUrl, code)
|
||||
.then((affiliationOptions) => {
|
||||
this.setState({
|
||||
affiliationOptions: affiliationOptions,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateUserField(key, value) {
|
||||
this.props.onUpdateUserField(key, value);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{
|
||||
this.props.application?.affiliationUrl === "" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={this.props.labelSpan}>
|
||||
{Setting.getLabel(i18next.t("user:Address"), i18next.t("user:Address - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={24 - this.props.labelSpan} >
|
||||
<Cascader style={{width: "100%", maxWidth: "400px"}} value={this.props.user.address} options={this.state.addressOptions} onChange={value => {
|
||||
this.updateUserField("address", value);
|
||||
this.updateUserField("affiliation", "");
|
||||
this.updateUserField("score", 0);
|
||||
this.getAffiliationOptions(this.props.application, this.props.user);
|
||||
}} placeholder={i18next.t("signup:Please input your address!")} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={this.props.labelSpan}>
|
||||
{Setting.getLabel(i18next.t("user:Affiliation"), i18next.t("user:Affiliation - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{
|
||||
this.props.application?.affiliationUrl === "" ? (
|
||||
<Input value={this.props.user.affiliation} onChange={e => {
|
||||
this.updateUserField("affiliation", e.target.value);
|
||||
}} />
|
||||
) : (
|
||||
<Select virtual={false} style={{width: "100%"}} value={this.props.user.affiliation}
|
||||
onChange={(value => {
|
||||
const name = value;
|
||||
const affiliationOption = Setting.getArrayItem(this.state.affiliationOptions, "name", name);
|
||||
const id = affiliationOption.id;
|
||||
this.updateUserField("affiliation", name);
|
||||
this.updateUserField("score", id);
|
||||
})}
|
||||
options={[Setting.getOption(`(${i18next.t("general:empty")})`, "")].concat(this.state.affiliationOptions.map((affiliationOption) => Setting.getOption(affiliationOption.name, affiliationOption.name))
|
||||
)} />
|
||||
)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AffiliationSelect;
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import {Select} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import * as Setting from "../../Setting";
|
||||
import React from "react";
|
||||
|
||||
export const CountryCodeSelect = (props) => {
|
66
web/src/common/select/LanguageSelect.js
Normal file
66
web/src/common/select/LanguageSelect.js
Normal file
@ -0,0 +1,66 @@
|
||||
// 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 from "react";
|
||||
import * as Setting from "../../Setting";
|
||||
import {Dropdown} from "antd";
|
||||
import "../../App.less";
|
||||
import {GlobalOutlined} from "@ant-design/icons";
|
||||
|
||||
function flagIcon(country, alt) {
|
||||
return (
|
||||
<img width={24} alt={alt} src={`${Setting.StaticBaseUrl}/flag-icons/${country}.svg`} />
|
||||
);
|
||||
}
|
||||
|
||||
class LanguageSelect extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
languages: props.languages ?? Setting.Countries.map(item => item.key),
|
||||
};
|
||||
|
||||
Setting.Countries.forEach((country) => {
|
||||
new Image().src = `${Setting.StaticBaseUrl}/flag-icons/${country.country}.svg`;
|
||||
});
|
||||
}
|
||||
|
||||
items = Setting.Countries.map((country) => Setting.getItem(country.label, country.key, flagIcon(country.country, country.alt)));
|
||||
|
||||
getOrganizationLanguages(languages) {
|
||||
const select = [];
|
||||
for (const language of languages) {
|
||||
this.items.map((item, index) => item.key === language ? select.push(item) : null);
|
||||
}
|
||||
return select;
|
||||
}
|
||||
|
||||
render() {
|
||||
const languageItems = this.getOrganizationLanguages(this.state.languages);
|
||||
const onClick = (e) => {
|
||||
Setting.setLanguage(e.key);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown menu={{items: languageItems, onClick}} >
|
||||
<div className="select-box" style={{display: languageItems.length === 0 ? "none" : null, ...this.props.style}} >
|
||||
<GlobalOutlined style={{fontSize: "24px", color: "#4d4d4d"}} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LanguageSelect;
|
62
web/src/common/select/RegionSelect.js
Normal file
62
web/src/common/select/RegionSelect.js
Normal file
@ -0,0 +1,62 @@
|
||||
// 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 from "react";
|
||||
import * as Setting from "../../Setting";
|
||||
import {Select} from "antd";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
class RegionSelect extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
this.props.onChange(e);
|
||||
this.setState({value: e});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Select virtual={false}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{width: "100%"}}
|
||||
defaultValue={this.props.defaultValue || undefined}
|
||||
placeholder="Please select country/region"
|
||||
onChange={(value => {this.onChange(value);})}
|
||||
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
filterSort={(optionA, optionB) =>
|
||||
(optionA?.label ?? "").toLowerCase().localeCompare((optionB?.label ?? "").toLowerCase())
|
||||
}
|
||||
>
|
||||
{
|
||||
Setting.getCountryCodeData().map((item) => (
|
||||
<Option key={item.code} value={item.code} label={`${item.name} (${item.code})`} >
|
||||
{Setting.getCountryImage(item)}
|
||||
{`${item.name} (${item.code})`}
|
||||
</Option>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RegionSelect;
|
94
web/src/common/select/ThemeSelect.js
Normal file
94
web/src/common/select/ThemeSelect.js
Normal file
@ -0,0 +1,94 @@
|
||||
// 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 React from "react";
|
||||
import * as Setting from "../../Setting";
|
||||
import {Dropdown} from "antd";
|
||||
import "../../App.less";
|
||||
import i18next from "i18next";
|
||||
import {CheckOutlined} from "@ant-design/icons";
|
||||
import {CompactTheme, DarkTheme, Light} from "antd-token-previewer/es/icons";
|
||||
|
||||
export const Themes = [
|
||||
{label: "Default", key: "default", icon: <Light style={{fontSize: "24px", color: "#4d4d4d"}} />}, // i18next.t("theme:Default")
|
||||
{label: "Dark", key: "dark", icon: <DarkTheme style={{fontSize: "24px", color: "#4d4d4d"}} />}, // i18next.t("theme:Dark")
|
||||
{label: "Compact", key: "compact", icon: <CompactTheme style={{fontSize: "24px", color: "#4d4d4d"}} />}, // i18next.t("theme:Compact")
|
||||
];
|
||||
|
||||
function getIcon(themeKey) {
|
||||
if (themeKey?.includes("dark")) {
|
||||
return Themes.find(t => t.key === "dark").icon;
|
||||
} else if (themeKey?.includes("default")) {
|
||||
return Themes.find(t => t.key === "default").icon;
|
||||
}
|
||||
}
|
||||
|
||||
class ThemeSelect extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
icon = getIcon(this.props.themeAlgorithm);
|
||||
|
||||
getThemeItems() {
|
||||
return Themes.map((theme) => Setting.getItem(
|
||||
<div style={{display: "flex", justifyContent: "space-between"}}>
|
||||
<div>{i18next.t(`theme:${theme.label}`)}</div>
|
||||
{this.props.themeAlgorithm.includes(theme.key) ? <CheckOutlined style={{marginLeft: "5px"}} /> : null}
|
||||
</div>,
|
||||
theme.key, theme.icon));
|
||||
}
|
||||
|
||||
render() {
|
||||
const onClick = (e) => {
|
||||
let nextTheme;
|
||||
if (e.key === "compact") {
|
||||
if (this.props.themeAlgorithm.includes("compact")) {
|
||||
nextTheme = this.props.themeAlgorithm.filter((theme) => theme !== "compact");
|
||||
} else {
|
||||
nextTheme = [...this.props.themeAlgorithm, "compact"];
|
||||
}
|
||||
} else {
|
||||
if (!this.props.themeAlgorithm.includes(e.key)) {
|
||||
if (e.key === "dark") {
|
||||
nextTheme = [...this.props.themeAlgorithm.filter((theme) => theme !== "default"), e.key];
|
||||
} else {
|
||||
nextTheme = [...this.props.themeAlgorithm.filter((theme) => theme !== "dark"), e.key];
|
||||
}
|
||||
} else {
|
||||
nextTheme = [...this.props.themeAlgorithm];
|
||||
}
|
||||
}
|
||||
|
||||
this.icon = getIcon(nextTheme);
|
||||
this.props.onChange(nextTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown menu={{
|
||||
items: this.getThemeItems(),
|
||||
onClick,
|
||||
selectable: true,
|
||||
multiple: true,
|
||||
selectedKeys: [...this.props.themeAlgorithm],
|
||||
}}>
|
||||
<div className="select-box">
|
||||
{this.icon}
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ThemeSelect;
|
Reference in New Issue
Block a user