feat: support customize theme (#1500)

* refactor: simplify functions and improve variable naming

* feat: add themeEditor component

* feat: support customize theme

* chore: resolve conflict and add LICENCE

* chore: format code

* refactor: use icon replace background url

* feat: improve organization and application theme editor
This commit is contained in:
Yaodong Yu
2023-02-01 22:06:40 +08:00
committed by GitHub
parent b47baa06e1
commit 95b32d5ebf
28 changed files with 13025 additions and 12126 deletions

View File

@ -57,23 +57,24 @@ type Application struct {
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
ExpireInHours int `json:"expireInHours"`
RefreshExpireInHours int `json:"refreshExpireInHours"`
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
FormCss string `xorm:"text" json:"formCss"`
FormOffset int `json:"formOffset"`
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
ExpireInHours int `json:"expireInHours"`
RefreshExpireInHours int `json:"refreshExpireInHours"`
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
ThemeData *ThemeData `xorm:"json" json:"themeData"`
FormCss string `xorm:"text" json:"formCss"`
FormOffset int `json:"formOffset"`
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
}
func GetApplicationCount(owner, field, value string) int {

View File

@ -31,25 +31,34 @@ type AccountItem struct {
ModifyRule string `json:"modifyRule"`
}
type ThemeData struct {
ThemeType string `xorm:"varchar(30)" json:"themeType"`
ColorPrimary string `xorm:"varchar(10)" json:"colorPrimary"`
BorderRadius int `xorm:"int" json:"borderRadius"`
IsCompact bool `xorm:"bool" json:"isCompact"`
IsEnabled bool `xorm:"bool" json:"isEnabled"`
}
type Organization struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"`
Favicon string `xorm:"varchar(100)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PhonePrefix string `xorm:"varchar(10)" json:"phonePrefix"`
DefaultAvatar string `xorm:"varchar(100)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
Tags []string `xorm:"mediumtext" json:"tags"`
Languages []string `xorm:"varchar(255)" json:"languages"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
InitScore int `json:"initScore"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"`
Favicon string `xorm:"varchar(100)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PhonePrefix string `xorm:"varchar(10)" json:"phonePrefix"`
DefaultAvatar string `xorm:"varchar(100)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
Tags []string `xorm:"mediumtext" json:"tags"`
Languages []string `xorm:"varchar(255)" json:"languages"`
ThemeData *ThemeData `xorm:"json" json:"themeData"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
InitScore int `json:"initScore"`
EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"`
AccountItems []*AccountItem `xorm:"varchar(3000)" json:"accountItems"`
}

View File

@ -6,10 +6,13 @@
"@ant-design/icons": "^4.7.0",
"@craco/craco": "^6.4.5",
"@crowdin/cli": "^3.7.10",
"@ctrl/tinycolor": "^3.5.0",
"@emotion/react": "^11.10.5",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"antd": "5.1.2",
"antd": "5.1.6",
"antd-token-previewer": "^1.1.0-22",
"codemirror": "^5.61.1",
"copy-to-clipboard": "^3.3.1",
"core-js": "^3.25.0",

View File

@ -17,7 +17,7 @@ import "./App.less";
import {Helmet} from "react-helmet";
import * as Setting from "./Setting";
import {BarsOutlined, DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
import {Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result, theme} from "antd";
import {Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result} from "antd";
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
import OrganizationListPage from "./OrganizationListPage";
import OrganizationEditPage from "./OrganizationEditPage";
@ -83,8 +83,8 @@ class App extends Component {
account: undefined,
uri: null,
menuVisible: false,
themeAlgorithm: null,
logo: null,
themeAlgorithm: ["default"],
themeData: Setting.ThemeDefault,
};
Setting.initServerUrl();
@ -99,16 +99,6 @@ class App extends Component {
this.getAccount();
}
componentDidMount() {
localStorage.getItem("theme") ?
this.setState({"themeAlgorithm": this.getTheme()}) : this.setState({"themeAlgorithm": theme.defaultAlgorithm});
this.setState({"logo": Setting.getLogo(localStorage.getItem("theme"))});
addEventListener("themeChange", (e) => {
this.setState({"themeAlgorithm": this.getTheme()});
this.setState({"logo": Setting.getLogo(localStorage.getItem("theme"))});
});
}
componentDidUpdate() {
// eslint-disable-next-line no-restricted-globals
const uri = location.pathname;
@ -198,8 +188,12 @@ class App extends Component {
return "";
}
getTheme() {
return Setting.Themes.find(t => t.key === localStorage.getItem("theme"))["style"];
getLogo(themes) {
if (themes.includes("dark")) {
return `${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256_dark.png`;
} else {
return `${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png`;
}
}
setLanguage(account) {
@ -209,6 +203,19 @@ class App extends Component {
}
}
setTheme = (theme, initThemeAlgorithm) => {
this.setState({
themeData: theme,
});
if (initThemeAlgorithm) {
this.setState({
logo: this.getLogo(Setting.getAlgorithmNames(theme)),
themeAlgorithm: Setting.getAlgorithmNames(theme),
});
}
};
getAccount() {
const params = new URLSearchParams(this.props.location.search);
@ -233,7 +240,9 @@ class App extends Component {
if (res.status === "ok") {
account = res.data;
account.organization = res.data2;
this.setLanguage(account);
this.setTheme(Setting.getThemeData(account.organization), Conf.InitThemeAlgorithm);
} else {
if (res.data !== "Please login first") {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
@ -259,6 +268,7 @@ class App extends Component {
this.setState({
account: null,
themeAlgorithm: ["default"],
});
Setting.showMessage("success", i18next.t("application:Logged out successfully"));
@ -282,14 +292,6 @@ class App extends Component {
});
}
handleRightDropdownClick(e) {
if (e.key === "/account") {
this.props.history.push("/account");
} else if (e.key === "/logout") {
this.logout();
}
}
renderAvatar() {
if (this.state.account.avatar === "") {
return (
@ -313,13 +315,18 @@ class App extends Component {
));
items.push(Setting.getItem(<><LogoutOutlined />&nbsp;&nbsp;{i18next.t("account:Logout")}</>,
"/logout"));
const onClick = this.handleRightDropdownClick.bind(this);
const onClick = (e) => {
if (e.key === "/account") {
this.props.history.push("/account");
} else if (e.key === "/logout") {
this.logout();
}
};
return (
<Dropdown key="/rightDropDown" menu={{items, onClick}} className="rightDropDown">
<div className="ant-dropdown-link" style={{float: "right", cursor: "pointer"}}>
&nbsp;
&nbsp;
<Dropdown key="/rightDropDown" menu={{items, onClick}} >
<div className="rightDropDown">
{
this.renderAvatar()
}
@ -334,34 +341,30 @@ class App extends Component {
);
}
renderAccount() {
const res = [];
renderAccountMenu() {
if (this.state.account === undefined) {
return null;
} else if (this.state.account === null) {
// res.push(
// <Menu.Item key="/signup" style={{float: 'right', marginRight: '20px'}}>
// <Link to="/signup">
// {i18next.t("account:Sign Up")}
// </Link>
// </Menu.Item>
// );
// res.push(
// <Menu.Item key="/login" style={{float: 'right'}}>
// <Link to="/login">
// {i18next.t("account:Login")}
// </Link>
// </Menu.Item>
// );
return null;
} else {
res.push(this.renderRightDropdown());
return (
<React.Fragment>
{this.renderRightDropdown()}
<SelectThemeBox
themeAlgorithm={this.state.themeAlgorithm}
onChange={(nextThemeAlgorithm) => {
this.setState({
themeAlgorithm: nextThemeAlgorithm,
logo: this.getLogo(nextThemeAlgorithm),
});
}} />
<SelectLanguageBox languages={this.state.account.organization.languages} />
</React.Fragment>
);
}
return res;
}
renderMenu() {
getMenuItems() {
const res = [];
if (this.state.account === null || this.state.account === undefined) {
@ -487,63 +490,53 @@ class App extends Component {
renderRouter() {
return (
<ConfigProvider theme={{
token: {
colorPrimary: "rgb(89,54,213)",
colorInfo: "rgb(89,54,213)",
},
algorithm: this.state.themeAlgorithm,
}}>
<div>
<Switch>
<Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
<Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
<Route exact path="/" render={(props) => this.renderLoginIfNotLoggedIn(<HomePage account={this.state.account} {...props} />)} />
<Route exact path="/account" render={(props) => this.renderLoginIfNotLoggedIn(<AccountPage account={this.state.account} {...props} />)} />
<Route exact path="/organizations" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationListPage account={this.state.account} {...props} />)} />
<Route exact path="/organizations/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationEditPage account={this.state.account} {...props} />)} />
<Route exact path="/organizations/:organizationName/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
<Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
<Route exact path="/users/:organizationName/:userName" render={(props) => <UserEditPage account={this.state.account} {...props} />} />
<Route exact path="/roles" render={(props) => this.renderLoginIfNotLoggedIn(<RoleListPage account={this.state.account} {...props} />)} />
<Route exact path="/roles/:organizationName/:roleName" render={(props) => this.renderLoginIfNotLoggedIn(<RoleEditPage account={this.state.account} {...props} />)} />
<Route exact path="/permissions" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionListPage account={this.state.account} {...props} />)} />
<Route exact path="/permissions/:organizationName/:permissionName" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionEditPage account={this.state.account} {...props} />)} />
<Route exact path="/models" render={(props) => this.renderLoginIfNotLoggedIn(<ModelListPage account={this.state.account} {...props} />)} />
<Route exact path="/models/:organizationName/:modelName" render={(props) => this.renderLoginIfNotLoggedIn(<ModelEditPage account={this.state.account} {...props} />)} />
<Route exact path="/adapters" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterListPage account={this.state.account} {...props} />)} />
<Route exact path="/adapters/:organizationName/:adapterName" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterEditPage account={this.state.account} {...props} />)} />
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)} />
<Route exact path="/providers/:organizationName/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} />
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)} />
<Route exact path="/applications/:organizationName/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)} />
<Route exact path="/resources" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceListPage account={this.state.account} {...props} />)} />
{/* <Route exact path="/resources/:resourceName" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceEditPage account={this.state.account} {...props} />)}/>*/}
<Route exact path="/ldap/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)} />
<Route exact path="/ldap/sync/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapSyncPage account={this.state.account} {...props} />)} />
<Route exact path="/tokens" render={(props) => this.renderLoginIfNotLoggedIn(<TokenListPage account={this.state.account} {...props} />)} />
<Route exact path="/sessions" render={(props) => this.renderLoginIfNotLoggedIn(<SessionListPage account={this.state.account} {...props} />)} />
<Route exact path="/tokens/:tokenName" render={(props) => this.renderLoginIfNotLoggedIn(<TokenEditPage account={this.state.account} {...props} />)} />
<Route exact path="/webhooks" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookListPage account={this.state.account} {...props} />)} />
<Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)} />
<Route exact path="/syncers" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerListPage account={this.state.account} {...props} />)} />
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)} />
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)} />
<Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products" render={(props) => this.renderLoginIfNotLoggedIn(<ProductListPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)} />
<Route exact path="/payments" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} />
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)} />
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo account={this.state.account} {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
</div>
</ConfigProvider>
<Switch>
<Route exact path="/result" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
<Route exact path="/result/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ResultPage {...props} />)} />
<Route exact path="/" render={(props) => this.renderLoginIfNotLoggedIn(<HomePage account={this.state.account} {...props} />)} />
<Route exact path="/account" render={(props) => this.renderLoginIfNotLoggedIn(<AccountPage account={this.state.account} {...props} />)} />
<Route exact path="/organizations" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationListPage account={this.state.account} {...props} />)} />
<Route exact path="/organizations/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationEditPage account={this.state.account} onChangeTheme={this.setTheme} {...props} />)} />
<Route exact path="/organizations/:organizationName/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
<Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
<Route exact path="/users/:organizationName/:userName" render={(props) => <UserEditPage account={this.state.account} {...props} />} />
<Route exact path="/roles" render={(props) => this.renderLoginIfNotLoggedIn(<RoleListPage account={this.state.account} {...props} />)} />
<Route exact path="/roles/:organizationName/:roleName" render={(props) => this.renderLoginIfNotLoggedIn(<RoleEditPage account={this.state.account} {...props} />)} />
<Route exact path="/permissions" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionListPage account={this.state.account} {...props} />)} />
<Route exact path="/permissions/:organizationName/:permissionName" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionEditPage account={this.state.account} {...props} />)} />
<Route exact path="/models" render={(props) => this.renderLoginIfNotLoggedIn(<ModelListPage account={this.state.account} {...props} />)} />
<Route exact path="/models/:organizationName/:modelName" render={(props) => this.renderLoginIfNotLoggedIn(<ModelEditPage account={this.state.account} {...props} />)} />
<Route exact path="/adapters" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterListPage account={this.state.account} {...props} />)} />
<Route exact path="/adapters/:organizationName/:adapterName" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterEditPage account={this.state.account} {...props} />)} />
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)} />
<Route exact path="/providers/:organizationName/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} />
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)} />
<Route exact path="/applications/:organizationName/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)} />
<Route exact path="/resources" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceListPage account={this.state.account} {...props} />)} />
{/* <Route exact path="/resources/:resourceName" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceEditPage account={this.state.account} {...props} />)}/>*/}
<Route exact path="/ldap/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)} />
<Route exact path="/ldap/sync/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapSyncPage account={this.state.account} {...props} />)} />
<Route exact path="/tokens" render={(props) => this.renderLoginIfNotLoggedIn(<TokenListPage account={this.state.account} {...props} />)} />
<Route exact path="/sessions" render={(props) => this.renderLoginIfNotLoggedIn(<SessionListPage account={this.state.account} {...props} />)} />
<Route exact path="/tokens/:tokenName" render={(props) => this.renderLoginIfNotLoggedIn(<TokenEditPage account={this.state.account} {...props} />)} />
<Route exact path="/webhooks" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookListPage account={this.state.account} {...props} />)} />
<Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)} />
<Route exact path="/syncers" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerListPage account={this.state.account} {...props} />)} />
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)} />
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)} />
<Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products" render={(props) => this.renderLoginIfNotLoggedIn(<ProductListPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)} />
<Route exact path="/payments" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} />
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)} />
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo account={this.state.account} {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
);
}
@ -560,85 +553,56 @@ class App extends Component {
};
renderContent() {
if (!Setting.isMobile()) {
return (
<Layout id="parent-area">
<Header style={{marginBottom: "3px", paddingInline: 0, backgroundColor: this.state.themeAlgorithm === theme.darkAlgorithm ? "black" : "white"}}>
{
Setting.isMobile() ? null : (
<Link to={"/"}>
<div className="logo" style={{background: `url(${this.state.logo})`}} />
</Link>
)
}
return (
<Layout id="parent-area">
{/* https://github.com/ant-design/ant-design/issues/40394 ant design bug. If it will be fixed, we can delete the code for control the color of Header*/}
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: this.state.themeAlgorithm.includes("dark") ? "black" : "white"}}>
{Setting.isMobile() ? null : (
<Link to={"/"}>
<div className="logo" style={{background: `url(${this.getLogo(Setting.getAlgorithmNames(this.state.themeData))})`}} />
</Link>
)}
{Setting.isMobile() ?
<React.Fragment>
<Drawer title={i18next.t("general:Close")} placement="left" visible={this.state.menuVisible} onClose={this.onClose}>
<Menu
items={this.getMenuItems()}
mode={"inline"}
selectedKeys={[this.state.selectedMenuKey]}
style={{lineHeight: "64px"}}
onClick={this.onClose}
>
</Menu>
</Drawer>
<Button icon={<BarsOutlined />} onClick={this.showMenu} type="text">
{i18next.t("general:Menu")}
</Button>
</React.Fragment> :
<Menu
items={this.renderMenu()}
mode={(Setting.isMobile() && this.isStartPages()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{position: "absolute", left: "145px", backgroundColor: this.state.themeAlgorithm === theme.darkAlgorithm ? "black" : "white"}}
items={this.getMenuItems()}
mode={"horizontal"}
selectedKeys={[this.state.selectedMenuKey]}
style={{position: "absolute", left: "145px", right: "260px"}}
/>
{
this.renderAccount()
}
{this.state.account && <SelectThemeBox themes={this.state.account.organization.themes} />}
{this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />}
</Header>
<Content style={{alignItems: "stretch", display: "flex", flexDirection: "column"}}>
<Card className="content-warp-card">
{
this.renderRouter()
}
</Card>
</Content>
{
this.renderFooter()
}
</Layout>
);
} else {
return (
<Layout>
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: this.state.themeAlgorithm === theme.darkAlgorithm ? "black" : "white"}}>
{
Setting.isMobile() ? null : (
<Link to={"/"}>
<div className="logo" style={{background: `url(${this.state.logo})`}} />
</Link>
)
}
<Drawer title={i18next.t("general:Close")} placement="left" visible={this.state.menuVisible} onClose={this.onClose}>
<Menu
// theme="dark"
items={this.renderMenu()}
mode={(Setting.isMobile()) ? "inline" : "horizontal"}
selectedKeys={[`${this.state.selectedMenuKey}`]}
style={{lineHeight: "64px"}}
onClick={this.onClose}
>
</Menu>
</Drawer>
<Button icon={<BarsOutlined />} onClick={this.showMenu} type="text">
{i18next.t("general:Menu")}
</Button>
{
this.renderAccount()
}
{this.state.account && <SelectThemeBox themes={this.state.account.organization.themes} />}
{this.state.account && <SelectLanguageBox languages={this.state.account.organization.languages} />}
</Header>
<Content style={{display: "flex", flexDirection: "column"}} >{
this.renderRouter()}
</Content>
{this.renderFooter()}
</Layout>
);
}
{
this.renderAccountMenu()
}
</Header>
<Content style={{display: "flex", flexDirection: "column"}} >
{Setting.isMobile() ?
this.renderRouter() :
<Card className="content-warp-card">
{this.renderRouter()}
</Card>
}
</Content>
{this.renderFooter()}
</Layout>
);
}
renderFooter() {
// How to keep your footer where it belongs ?
// https://www.freecodecamp.org/neyarnws/how-to-keep-your-footer-where-it-belongs-59c6aa05c59c/
return (
<React.Fragment>
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
@ -647,7 +611,7 @@ class App extends Component {
textAlign: "center",
}
}>
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={this.state.logo} /></a>
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={this.getLogo(Setting.getAlgorithmNames(this.state.themeData))} /></a>
</Footer>
</React.Fragment>
);
@ -669,26 +633,35 @@ class App extends Component {
renderPage() {
if (this.isDoorPages()) {
return (
<React.Fragment>
<Layout id="parent-area">
<Content style={{display: "flex", justifyContent: "center"}}>
{
this.isEntryPages() ?
<EntryPage account={this.state.account} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />
:
<Switch>
<Route exact path="/callback" component={AuthCallback} />
<Route exact path="/callback/saml" component={SamlCallback} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
}
</Content>
<Layout id="parent-area">
<Content style={{display: "flex", justifyContent: "center"}}>
{
this.renderFooter()
this.isEntryPages() ?
<EntryPage
account={this.state.account}
theme={this.state.themeData}
onUpdateAccount={(account) => {
this.onUpdateAccount(account);
}}
updataThemeData={(nextThemeData) => {
this.setState({
themeData: nextThemeData,
});
localStorage.setItem("themeAlgorithm", Setting.getAlgorithmNames(nextThemeData).toString());
}}
/> :
<Switch>
<Route exact path="/callback" component={AuthCallback} />
<Route exact path="/callback/saml" component={SamlCallback} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
}
</Layout>
</React.Fragment>
</Content>
{
this.renderFooter()
}
</Layout>
);
}
@ -704,40 +677,24 @@ class App extends Component {
}
render() {
if (this.state.account === undefined || this.state.account === null) {
return (
<React.Fragment>
<Helmet>
<link rel="icon" href={"https://cdn.casdoor.com/static/favicon.png"} />
</Helmet>
<ConfigProvider theme={{
token: {
colorPrimary: "rgb(89,54,213)",
colorInfo: "rgb(89,54,213)",
},
algorithm: this.state.themeAlgorithm,
}}>
{
this.renderPage()
}
</ConfigProvider>
</React.Fragment>
);
}
const organization = this.state.account.organization;
return (
<React.Fragment>
<Helmet>
<title>{organization.displayName}</title>
<link rel="icon" href={organization.favicon} />
</Helmet>
{(this.state.account === undefined || this.state.account === null) ?
<Helmet>
<link rel="icon" href={"https://cdn.casdoor.com/static/favicon.png"} />
</Helmet> :
<Helmet>
<title>{this.state.account.organization?.displayName}</title>
<link rel="icon" href={this.state.account.organization?.favicon} />
</Helmet>
}
<ConfigProvider theme={{
token: {
colorPrimary: "rgb(89,54,213)",
colorInfo: "rgb(89,54,213)",
colorPrimary: this.state.themeData.colorPrimary,
colorInfo: this.state.themeData.colorPrimary,
borderRadius: this.state.themeData.borderRadius,
},
algorithm: this.state.themeAlgorithm,
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
}}>
{
this.renderPage()

View File

@ -1,8 +1,6 @@
/* stylelint-disable at-rule-name-case */
/* stylelint-disable selector-class-pattern */
@StaticBaseUrl: "https://cdn.casbin.org";
.App {
text-align: center;
}
@ -45,34 +43,13 @@ img {
margin-bottom: 30px;
}
.language-box {
background: url("@{StaticBaseUrl}/img/muti_language.svg");
background-size: 25px, 25px;
background-position: center;
background-repeat: no-repeat;
.select-box {
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
width: 45px;
height: 100%;
float: right;
cursor: pointer;
&:hover {
background-color: #f5f5f5;
}
}
.login-form .language-box {
height: 65px;
}
.theme-box {
background: url("@{StaticBaseUrl}/img/muti_language.svg");
background-size: 25px, 25px;
background-position: center !important;
background-repeat: no-repeat !important;
border-radius: 5px;
width: 45px;
height: 100%;
height: 64px;
float: right;
cursor: pointer;
@ -82,7 +59,12 @@ img {
}
.rightDropDown {
display: flex;
align-items: center;
justify-content: center;
border-radius: 7px;
float: right;
cursor: pointer;
&:hover {
background-color: #f5f5f5;
@ -151,6 +133,4 @@ img {
.ant-menu-horizontal {
border-bottom: none !important;
margin-right: 30px;
right: 230px;
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Popover, Radio, Result, Row, Select, Switch, Upload} from "antd";
import {Button, Card, Col, ConfigProvider, Input, Popover, Radio, Result, Row, Select, Switch, Upload} from "antd";
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend";
@ -32,6 +32,7 @@ import copy from "copy-to-clipboard";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import ThemeEditor from "./common/theme/ThemeEditor";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/htmlmixed/htmlmixed");
@ -709,6 +710,31 @@ class ApplicationEditPage extends React.Component {
: null}
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("theme:Theme"), i18next.t("theme:Theme - Tooltip"))} :
</Col>
<Col span={22} style={{marginTop: "5px"}}>
<Row>
<Radio.Group value={this.state.application.themeData?.isEnabled ?? false} onChange={e => {
const {_, ...theme} = this.state.application.themeData ?? {...Setting.ThemeDefault, isEnabled: false};
this.updateApplicationField("themeData", {...theme, isEnabled: e.target.value});
}} >
<Radio.Button value={false}>{i18next.t("application:Follow organization theme")}</Radio.Button>
<Radio.Button value={true}>{i18next.t("theme:Customize theme")}</Radio.Button>
</Radio.Group>
</Row>
{
this.state.application.themeData?.isEnabled ?
<Row style={{marginTop: "20px"}}>
<ThemeEditor themeData={this.state.application.themeData} onThemeChange={(_, nextThemeData) => {
const {isEnabled} = this.state.application.themeData ?? {...Setting.ThemeDefault, isEnabled: false};
this.updateApplicationField("themeData", {...nextThemeData, isEnabled});
}} />
</Row> : null
}
</Col>
</Row>
{
!this.state.application.enableSignUp ? null : (
<Row style={{marginTop: "20px"}} >
@ -738,6 +764,7 @@ class ApplicationEditPage extends React.Component {
}
renderSignupSigninPreview() {
const themeData = this.state.application.themeData;
let signUpUrl = `/signup/${this.state.application.name}`;
const signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${this.state.application.redirectUris[0]}&scope=read&state=casdoor`;
const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "97%", width: "100%", background: "rgba(0,0,0,0.4)"};
@ -756,20 +783,28 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy signup page URL")}
</Button>
<br />
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", overflow: "auto"}}>
{
this.state.application.enablePassword ? (
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
<SignupPage application={this.state.application} preview = "auto" />
</div>
) : (
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
<LoginPage type={"login"} mode={"signup"} application={this.state.application} preview = "auto" />
</div>
)
}
<div style={{overflow: "auto", ...maskStyle}} />
</div>
<ConfigProvider theme={{
token: {
colorPrimary: themeData.colorPrimary,
colorInfo: themeData.colorPrimary,
borderRadius: themeData.borderRadius,
},
}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", overflow: "auto"}}>
{
this.state.application.enablePassword ? (
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
<SignupPage application={this.state.application} preview = "auto" />
</div>
) : (
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
<LoginPage type={"login"} mode={"signup"} application={this.state.application} preview = "auto" />
</div>
)
}
<div style={{overflow: "auto", ...maskStyle}} />
</div>
</ConfigProvider>
</Col>
<Col span={previewGrid}>
<Button style={{marginBottom: "10px", marginTop: Setting.isMobile() ? "15px" : "0"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
@ -780,18 +815,27 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy signin page URL")}
</Button>
<br />
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", overflow: "auto"}}>
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} preview = "auto" />
<ConfigProvider theme={{
token: {
colorPrimary: themeData.colorPrimary,
colorInfo: themeData.colorPrimary,
borderRadius: themeData.borderRadius,
},
}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", overflow: "auto"}}>
<div className="loginBackground" style={{backgroundImage: `url(${this.state.application?.formBackgroundUrl})`, overflow: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} preview = "auto" />
</div>
<div style={{overflow: "auto", ...maskStyle}} />
</div>
<div style={{overflow: "auto", ...maskStyle}} />
</div>
</ConfigProvider>
</Col>
</React.Fragment>
);
}
renderPromptPreview() {
const themeData = this.state.application.themeData;
const promptUrl = `/prompt/${this.state.application.name}`;
const maskStyle = {position: "absolute", top: "0px", left: "0px", zIndex: 10, height: "100%", width: "100%", background: "rgba(0,0,0,0.4)"};
return (
@ -804,10 +848,18 @@ class ApplicationEditPage extends React.Component {
{i18next.t("application:Copy prompt page URL")}
</Button>
<br />
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<PromptPage application={this.state.application} account={this.props.account} />
<div style={maskStyle} />
</div>
<ConfigProvider theme={{
token: {
colorPrimary: themeData.colorPrimary,
colorInfo: themeData.colorPrimary,
borderRadius: themeData.borderRadius,
},
}}>
<div style={{position: "relative", width: previewWidth, border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<PromptPage application={this.state.application} account={this.props.account} />
<div style={maskStyle} />
</div>
</ConfigProvider>
</Col>
);
}

View File

@ -17,5 +17,6 @@ export const GithubRepo = "https://github.com/casdoor/casdoor";
export const ForceLanguage = "";
export const DefaultLanguage = "en";
export const InitThemeAlgorithm = true;
export const EnableExtraPages = true;

View File

@ -52,32 +52,41 @@ class EntryPage extends React.Component {
}
}
getApplicationObj() {
return this.state.application || null;
}
render() {
const onUpdateApplication = (application) => {
this.setState({
application: application,
});
const themeData = application !== null ? Setting.getThemeData(application.organizationObj, application) : Setting.ThemeDefault;
this.props.updataThemeData(themeData);
};
return <div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
<Spin spinning={this.state.application === undefined} tip={i18next.t("login:Loading")} style={{margin: "0 auto"}} />
<Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/auto-signup/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signup"} onUpdateApplication={onUpdateApplication}{...props} />} />
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"saml"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage {...this.props} application={this.state.application} type={"cas"} mode={"signup"} {...props} />);}} />
</Switch>
</div>;
return (
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
<Spin spinning={this.state.application === undefined} tip={i18next.t("login:Loading")} style={{margin: "0 auto"}} />
<Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/auto-signup/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signup"} onUpdateApplication={onUpdateApplication}{...props} />} />
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"saml"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage {...this.props} application={this.state.application} type={"cas"} mode={"signup"} {...props} />);}} />
</Switch>
</div>
);
}
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd";
import {Button, Card, Col, Input, InputNumber, Radio, Row, Select, Switch} from "antd";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as LdapBackend from "./backend/LdapBackend";
@ -22,6 +22,7 @@ import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons";
import LdapTable from "./LdapTable";
import AccountTable from "./AccountTable";
import ThemeEditor from "./common/theme/ThemeEditor";
const {Option} = Select;
@ -316,6 +317,31 @@ class OrganizationEditPage extends React.Component {
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("theme:Theme"), i18next.t("theme:Theme - Tooltip"))} :
</Col>
<Col span={22} style={{marginTop: "5px"}}>
<Row>
<Radio.Group value={this.state.organization.themeData?.isEnabled ?? false} onChange={e => {
const {_, ...theme} = this.state.organization.themeData ?? {...Setting.ThemeDefault, isEnabled: false};
this.updateOrganizationField("themeData", {...theme, isEnabled: e.target.value});
}} >
<Radio.Button value={false}>{i18next.t("organization:Follow global theme")}</Radio.Button>
<Radio.Button value={true}>{i18next.t("theme:Customize theme")}</Radio.Button>
</Radio.Group>
</Row>
{
this.state.organization.themeData?.isEnabled ?
<Row style={{marginTop: "20px"}}>
<ThemeEditor themeData={this.state.organization.themeData} onThemeChange={(_, nextThemeData) => {
const {isEnabled} = this.state.organization.themeData ?? {...Setting.ThemeDefault, isEnabled: false};
this.updateOrganizationField("themeData", {...nextThemeData, isEnabled});
}} />
</Row> : null
}
</Col>
</Row>
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} :
@ -341,6 +367,11 @@ class OrganizationEditPage extends React.Component {
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
if (this.props.account.organization.name === this.state.organizationName) {
this.props.onChangeTheme(Setting.getThemeData(this.state.organization));
}
this.setState({
organizationName: this.state.organization.name,
});

View File

@ -16,6 +16,7 @@ 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 (
@ -30,6 +31,10 @@ class SelectLanguageBox extends React.Component {
classes: props,
languages: props.languages ?? ["en", "zh", "es", "fr", "de", "ja", "ko", "ru"],
};
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)));
@ -50,7 +55,9 @@ class SelectLanguageBox extends React.Component {
return (
<Dropdown menu={{items: languageItems, onClick}} >
<div className="language-box" style={{display: languageItems.length === 0 ? "none" : null, ...this.props.style}} />
<div className="select-box" style={{display: languageItems.length === 0 ? "none" : null}} >
<GlobalOutlined style={{fontSize: "24px"}} />
</div>
</Dropdown>
);
}

View File

@ -1,4 +1,4 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
// 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.
@ -17,62 +17,76 @@ 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";
function themeIcon(themeKey) {
return <img width={24} alt={themeKey} src={getLogoURL(themeKey)} />;
}
export const Themes = [
{label: "Default", key: "default", icon: <Light style={{fontSize: "24px"}} />}, // i18next.t("theme:Default")
{label: "Dark", key: "dark", icon: <DarkTheme style={{fontSize: "24px"}} />}, // i18next.t("theme:Dark")
{label: "Compact", key: "compact", icon: <CompactTheme style={{fontSize: "24px"}} />}, // i18next.t("theme:Compact")
];
function getLogoURL(themeKey) {
if (themeKey) {
return Setting.Themes.find(t => t.key === themeKey)["selectThemeLogo"];
} else {
return Setting.Themes.find(t => t.key === localStorage.getItem("theme"))["selectThemeLogo"];
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 SelectThemeBox extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
themes: props.theme ?? ["Default", "Dark", "Compact"],
icon: null,
};
}
items = this.getThemes();
icon = getIcon(this.props.themeAlgorithm);
componentDidMount() {
i18next.on("languageChanged", () => {
this.items = this.getThemes();
});
localStorage.getItem("theme") ? this.setState({"icon": getLogoURL()}) : this.setState({"icon": getLogoURL("Default")});
addEventListener("themeChange", (e) => {
this.setState({"icon": getLogoURL()});
});
}
getThemes() {
return Setting.Themes.map((theme) => Setting.getItem(i18next.t(`general:${theme.label}`), theme.key, themeIcon(theme.key)));
}
getOrganizationThemes(themes) {
const select = [];
for (const theme of themes) {
this.items.map((item, index) => item.key === theme ? select.push(item) : null);
}
return select;
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 themeItems = this.getOrganizationThemes(this.state.themes);
const onClick = (e) => {
Setting.setTheme(e.key);
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: themeItems, onClick}} >
<div className="theme-box" style={{display: themeItems.length === 0 ? "none" : null, background: `url(${this.state.icon})`, ...this.props.style}} />
<Dropdown menu={{
items: this.getThemeItems(),
onClick,
selectable: true,
multiple: true,
selectedKeys: [...this.props.themeAlgorithm],
}}
trigger={["click"]}>
<div className="select-box">
{this.icon}
</div>
</Dropdown>
);
}

View File

@ -43,12 +43,43 @@ export const Countries = [{label: "English", key: "en", country: "US", alt: "Eng
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
];
const {defaultAlgorithm, darkAlgorithm, compactAlgorithm} = theme;
export const ThemeDefault = {
themeType: "default",
colorPrimary: "#5734d3",
borderRadius: 6,
isCompact: false,
};
export const Themes = [{label: i18next.t("general:Dark"), key: "Dark", style: darkAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/dark.svg`},
{label: i18next.t("general:Compact"), key: "Compact", style: compactAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/compact.svg`},
{label: i18next.t("general:Default"), key: "Default", style: defaultAlgorithm, selectThemeLogo: `${StaticBaseUrl}/img/light.svg`},
];
export function getThemeData(organization, application) {
if (application?.themeData?.isEnabled) {
return application.themeData;
} else if (organization?.themeData?.isEnabled) {
return organization.themeData;
} else {
return ThemeDefault;
}
}
export function getAlgorithm(themeAlgorithmNames) {
return themeAlgorithmNames.map((algorithmName) => {
if (algorithmName === "dark") {
return theme.darkAlgorithm;
}
if (algorithmName === "compact") {
return theme.compactAlgorithm;
}
return theme.defaultAlgorithm;
});
}
export function getAlgorithmNames(themeData) {
const algorithms = [themeData?.themeType !== "dark" ? "default" : "dark"];
if (themeData?.isCompact === true) {
algorithms.push("compact");
}
return algorithms;
}
export const OtherProviderInfo = {
SMS: {
@ -532,10 +563,8 @@ export function isAgreementRequired(application) {
export function isDefaultTrue(application) {
const agreementItem = application.signupItems.find(item => item.name === "Agreement");
if (isAgreementRequired(application) && agreementItem.rule === "Signin (Default True)") {
return true;
}
return false;
return isAgreementRequired(application) && agreementItem.rule === "Signin (Default True)";
}
export function renderAgreement(required, onClick, noStyle, layout, initialValue) {
@ -670,13 +699,12 @@ export function getLanguage() {
export function setLanguage(language) {
localStorage.setItem("language", language);
changeMomentLanguage(language);
i18next.changeLanguage(language);
}
export function setTheme(themeKey) {
localStorage.setItem("theme", themeKey);
dispatchEvent(new Event("themeChange"));
dispatchEvent(new Event("changeTheme"));
}
export function getAcceptLanguage() {
@ -686,29 +714,6 @@ export function getAcceptLanguage() {
return i18next.language + ";q=0.9,en;q=0.8";
}
export function changeMomentLanguage(language) {
// if (language === "zh") {
// moment.locale("zh", {
// relativeTime: {
// future: "%s内",
// past: "%s前",
// s: "几秒",
// ss: "%d秒",
// m: "1分钟",
// mm: "%d分钟",
// h: "1小时",
// hh: "%d小时",
// d: "1天",
// dd: "%d天",
// M: "1个月",
// MM: "%d个月",
// y: "1年",
// yy: "%d年",
// },
// });
// }
}
export function getClickable(text) {
return (
<a onClick={() => {

View File

@ -77,10 +77,6 @@ class LoginPage extends React.Component {
Setting.showMessage("error", `Unknown authentication type: ${this.state.type}`);
}
}
Setting.Countries.forEach((country) => {
new Image().src = `${Setting.StaticBaseUrl}/flag-icons/${country.country}.svg`;
});
}
componentDidUpdate(prevProps, prevState, snapshot) {

View File

@ -71,8 +71,6 @@ export function deleteAdapter(Adapter) {
}
export function UpdatePolicy(owner, name, policy) {
// eslint-disable-next-line no-console
console.log(policy);
return fetch(`${Setting.ServerUrl}/api/update-policy?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",

View File

@ -0,0 +1,173 @@
// 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.
/** @jsxImportSource @emotion/react */
import {Input, Popover, Space, theme} from "antd";
import React, {useEffect, useMemo, useState} from "react";
import {css} from "@emotion/react";
import {TinyColor} from "@ctrl/tinycolor";
import ColorPanel from "antd-token-previewer/es/ColorPanel";
export const BLUE_COLOR = "#1677FF";
export const PINK_COLOR = "#ED4192";
export const GREEN_COLOR = "#00B96B";
export const COLORS = [
{
color: BLUE_COLOR,
},
{
color: "#5734d3",
},
{
color: "#9E339F",
},
{
color: PINK_COLOR,
},
{
color: "#E0282E",
},
{
color: "#F4801A",
},
{
color: "#F2BD27",
},
{
color: GREEN_COLOR,
},
];
export const PRESET_COLORS = COLORS.map(({color}) => color);
const {useToken} = theme;
const useStyle = () => {
const {token} = useToken();
return {
color: css `
width: ${token.controlHeightLG / 2}px;
height: ${token.controlHeightLG / 2}px;
border-radius: 100%;
cursor: pointer;
transition: all ${token.motionDurationFast};
display: inline-block;
& > input[type="radio"] {
width: 0;
height: 0;
opacity: 0;
}
`,
colorActive: css `
box-shadow: 0 0 0 1px ${token.colorBgContainer},
0 0 0 ${token.controlOutlineWidth * 2 + 1}px ${token.colorPrimary};
`,
};
};
const DebouncedColorPanel = ({color, onChange}) => {
const [value, setValue] = useState(color);
useEffect(() => {
const timeout = setTimeout(() => {
onChange?.(value);
}, 200);
return () => clearTimeout(timeout);
}, [value]);
useEffect(() => {
setValue(color);
}, [color]);
return <ColorPanel color={value} onChange={setValue} />;
};
export default function ColorPicker({value, onChange}) {
const style = useStyle();
const matchColors = useMemo(() => {
const valueStr = new TinyColor(value).toRgbString();
let existActive = false;
const colors = PRESET_COLORS.map((color) => {
const colorStr = new TinyColor(color).toRgbString();
const active = colorStr === valueStr;
existActive = existActive || active;
return {color, active, picker: false};
});
return [
...colors,
{
color: "conic-gradient(red, yellow, lime, aqua, blue, magenta, red)",
picker: true,
active: !existActive,
},
];
}, [value]);
return (
<Space size="large">
<Input
value={value}
onChange={(event) => {
onChange?.(event.target.value);
}}
style={{width: 120}}
/>
<Space size="middle">
{matchColors.map(({color, active, picker}) => {
let colorNode = (
<label
key={color}
css={[style.color, active && style.colorActive]}
style={{
background: color,
}}
onClick={() => {
if (!picker) {
onChange?.(color);
}
}}
>
<input type="radio" name={picker ? "picker" : "color"} tabIndex={picker ? -1 : 0} />
</label>
);
if (picker) {
colorNode = (
<Popover
key={color}
overlayInnerStyle={{padding: 0}}
content={
<DebouncedColorPanel color={value || ""} onChange={(c) => onChange?.(c)} />
}
trigger="click"
showArrow={false}
>
{colorNode}
</Popover>
);
}
return colorNode;
})}
</Space>
</Space>
);
}

View File

@ -0,0 +1,38 @@
// 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 {InputNumber, Slider, Space} from "antd";
export default function RadiusPicker({value, onChange}) {
return (
<Space size="large">
<InputNumber
value={value}
onChange={onChange}
style={{width: 120}}
min={0}
formatter={(val) => `${val}px`}
parser={(str) => (str ? parseFloat(str) : str)}
/>
<Slider
tooltip={{open: false}}
style={{width: 128}}
min={0}
value={value}
max={20}
onChange={onChange}
/>
</Space>
);
}

View File

@ -0,0 +1,108 @@
// 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 {Card, ConfigProvider, Form, Layout, Switch, theme} from "antd";
import ThemePicker from "./ThemePicker";
import ColorPicker from "./ColorPicker";
import RadiusPicker from "./RadiusPicker";
import * as React from "react";
import {GREEN_COLOR, PINK_COLOR} from "./ColorPicker";
import {Content} from "antd/es/layout/layout";
import i18next from "i18next";
import {useEffect} from "react";
import * as Setting from "../../Setting";
const ThemesInfo = {
default: {},
dark: {
borderRadius: 2,
},
lark: {
colorPrimary: GREEN_COLOR,
borderRadius: 4,
},
comic: {
colorPrimary: PINK_COLOR,
borderRadius: 16,
},
};
const onChange = () => {};
export default function ThemeEditor(props) {
const themeData = props.themeData ?? Setting.ThemeDefault;
const onThemeChange = props.onThemeChange ?? onChange;
const {isCompact, themeType, ...themeToken} = themeData;
const isLight = themeType !== "dark";
const [form] = Form.useForm();
const algorithmFn = React.useMemo(() => {
const algorithms = [isLight ? theme.defaultAlgorithm : theme.darkAlgorithm];
if (isCompact === true) {
algorithms.push(theme.compactAlgorithm);
}
return algorithms;
}, [isLight, isCompact]);
useEffect(() => {
const mergedData = Object.assign(Object.assign(Object.assign({}, Setting.ThemeDefault), {themeType}), ThemesInfo[themeType]);
onThemeChange(null, mergedData);
form.setFieldsValue(mergedData);
}, [themeType]);
return (
<ConfigProvider
theme={{
token: {
...themeToken,
},
hashed: true,
algorithm: algorithmFn,
}}
>
<Layout style={{width: "800px", backgroundColor: "white"}}>
<Content >
<Card
title={i18next.t("theme:Theme")}
>
<Form
form={form}
initialValues={themeData}
onValuesChange={onThemeChange}
labelCol={{span: 4}}
wrapperCol={{span: 20}}
style={{width: "800px"}}
>
<Form.Item label={i18next.t("theme:Theme")} name="themeType">
<ThemePicker />
</Form.Item>
<Form.Item label={i18next.t("theme:Primary color")} name="colorPrimary">
<ColorPicker />
</Form.Item>
<Form.Item label={i18next.t("theme:Border radius")} name="borderRadius">
<RadiusPicker />
</Form.Item>
<Form.Item label={i18next.t("theme:Is compact")} valuePropName="checked" name="isCompact">
<Switch />
</Form.Item>
</Form>
</Card>
</Content>
</Layout>
</ConfigProvider>
);
}

View File

@ -0,0 +1,104 @@
// 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.
/** @jsxImportSource @emotion/react */
import {css} from "@emotion/react";
import {Space, theme} from "antd";
import * as React from "react";
import i18next from "i18next";
import * as Setting from "../../Setting";
const {useToken} = theme;
export const THEMES = {
default: `${Setting.StaticBaseUrl}/img/theme_default.svg`,
dark: `${Setting.StaticBaseUrl}/img/theme_dark.svg`,
lark: `${Setting.StaticBaseUrl}/img/theme_lark.svg`,
comic: `${Setting.StaticBaseUrl}/img/theme_comic.svg`,
};
Object.values(THEMES).map(value => new Image().src = value);
const themeTypes = {
default: "Default", // i18next.t("theme:Default")
dark: "Dark", // i18next.t("theme:Dark")
lark: "Document", // i18next.t("theme:Document")
comic: "Blossom", // i18next.t("theme:Blossom")
};
const useStyle = () => {
const {token} = useToken();
return {
themeCard: css `
border-radius: ${token.borderRadius}px;
cursor: pointer;
transition: all ${token.motionDurationSlow};
overflow: hidden;
display: inline-block;
& > input[type="radio"] {
width: 0;
height: 0;
opacity: 0;
}
img {
vertical-align: top;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
}
&:focus-within,
&:hover {
transform: scale(1.04);
}
`,
themeCardActive: css `
box-shadow: 0 0 0 1px ${token.colorBgContainer},
0 0 0 ${token.controlOutlineWidth * 2 + 1}px ${token.colorPrimary};
&:hover:not(:focus-within) {
transform: scale(1);
}
`,
};
};
export default function ThemePicker({value, onChange}) {
const {token} = useToken();
const style = useStyle();
return (
<Space size={token.paddingLG}>
{Object.keys(THEMES).map((theme) => {
const url = THEMES[theme];
return (
<Space key={theme} direction="vertical" align="center">
<label
css={[style.themeCard, value === theme && style.themeCardActive]}
onClick={() => {
onChange?.(theme);
}}
>
<input type="radio" name="theme" />
<img src={url} alt={theme} />
</label>
<span>{i18next.t(`theme:${themeTypes[theme]}`)}</span>
</Space>
);
})}
</Space>
);
}

View File

@ -22,7 +22,6 @@ import ru from "./locales/ru/data.json";
import ja from "./locales/ja/data.json";
import es from "./locales/es/data.json";
import * as Conf from "./Conf";
import * as Setting from "./Setting";
import {initReactI18next} from "react-i18next";
const resources = {
@ -76,7 +75,6 @@ function initLanguage() {
}
}
}
Setting.changeMomentLanguage(language);
return language;
}

View File

@ -1,6 +1,5 @@
{
"account": {
"Login": "Anmelden",
"Logout": "Abmelden",
"My Account": "Mein Konto",
"Sign Up": "Registrieren"
@ -41,6 +40,7 @@
"Enable signup - Tooltip": "Whether to allow users to sign up",
"Failed to sign in": "Failed to sign in",
"File uploaded successfully": "Datei erfolgreich hochgeladen",
"Follow organization theme": "Follow organization theme",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
@ -158,10 +158,7 @@
"Click to Upload": "Click to Upload",
"Client IP": "Client-IP",
"Close": "Close",
"Compact": "Compact",
"Created time": "Erstellte Zeit",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Standard Avatar",
@ -341,6 +338,7 @@
"Default avatar": "Standard Avatar",
"Edit Organization": "Organisation bearbeiten",
"Favicon": "Févicon",
"Follow global theme": "Follow global theme",
"InitScore": "InitScore",
"Is profile public": "Is profile public",
"Is profile public - Tooltip": "Is profile public - Tooltip",
@ -690,6 +688,19 @@
"Unknown Version": "Unknown Version",
"Version": "Version"
},
"theme": {
"Blossom": "Blossom",
"Border radius": "Border radius",
"Compact": "Compact",
"Customize theme": "Customize theme",
"Dark": "Dark",
"Default": "Default",
"Document": "Document",
"Is compact": "Is compact",
"Primary color": "Primary color",
"Theme": "Theme",
"Theme - Tooltip": "Theme - Tooltip"
},
"token": {
"Access token": "Zugangs-Token",
"Authorization code": "Autorisierungscode",

View File

@ -1,6 +1,5 @@
{
"account": {
"Login": "Login",
"Logout": "Logout",
"My Account": "My Account",
"Sign Up": "Sign Up"
@ -41,6 +40,7 @@
"Enable signup - Tooltip": "Enable signup - Tooltip",
"Failed to sign in": "Failed to sign in",
"File uploaded successfully": "File uploaded successfully",
"Follow organization theme": "Follow organization theme",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
@ -158,10 +158,7 @@
"Click to Upload": "Click to Upload",
"Client IP": "Client IP",
"Close": "Close",
"Compact": "Compact",
"Created time": "Created time",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Default avatar",
@ -341,6 +338,7 @@
"Default avatar": "Default avatar",
"Edit Organization": "Edit Organization",
"Favicon": "Favicon",
"Follow global theme": "Follow global theme",
"InitScore": "InitScore",
"Is profile public": "Is profile public",
"Is profile public - Tooltip": "Is profile public - Tooltip",
@ -690,6 +688,19 @@
"Unknown Version": "Unknown Version",
"Version": "Version"
},
"theme": {
"Blossom": "Blossom",
"Border radius": "Border radius",
"Compact": "Compact",
"Customize theme": "Customize theme",
"Dark": "Dark",
"Default": "Default",
"Document": "Document",
"Is compact": "Is compact",
"Primary color": "Primary color",
"Theme": "Theme",
"Theme - Tooltip": "Theme - Tooltip"
},
"token": {
"Access token": "Access token",
"Authorization code": "Authorization code",

View File

@ -1,6 +1,5 @@
{
"account": {
"Login": "Iniciar sesión",
"Logout": "Cerrar sesión",
"My Account": "Mi cuenta",
"Sign Up": "Registrarme"
@ -41,6 +40,7 @@
"Enable signup - Tooltip": "Habilitar nuevos registros - Tooltip",
"Failed to sign in": "Failed to sign in",
"File uploaded successfully": "El archivo ha sido subido con éxito",
"Follow organization theme": "Follow organization theme",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
@ -158,10 +158,7 @@
"Click to Upload": "Click para subir archivo",
"Client IP": "IP del Cliente",
"Close": "Close",
"Compact": "Compact",
"Created time": "Fecha de creación",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Avatar por defecto",
@ -341,6 +338,7 @@
"Default avatar": "Avatar por defecto",
"Edit Organization": "Editar Organización",
"Favicon": "Favicon",
"Follow global theme": "Follow global theme",
"InitScore": "InitScore",
"Is profile public": "Es el perfil publico",
"Is profile public - Tooltip": "Es el perfil publico - Tooltip",
@ -690,6 +688,19 @@
"Unknown Version": "Unknown Version",
"Version": "Version"
},
"theme": {
"Blossom": "Blossom",
"Border radius": "Border radius",
"Compact": "Compact",
"Customize theme": "Customize theme",
"Dark": "Dark",
"Default": "Default",
"Document": "Document",
"Is compact": "Is compact",
"Primary color": "Primary color",
"Theme": "Theme",
"Theme - Tooltip": "Theme - Tooltip"
},
"token": {
"Access token": "Token de acceso",
"Authorization code": "Código de autorización",

View File

@ -1,6 +1,5 @@
{
"account": {
"Login": "Se connecter",
"Logout": "Déconnexion",
"My Account": "Mon Compte",
"Sign Up": "S'inscrire"
@ -41,6 +40,7 @@
"Enable signup - Tooltip": "Whether to allow users to sign up",
"Failed to sign in": "Failed to sign in",
"File uploaded successfully": "Fichier téléchargé avec succès",
"Follow organization theme": "Follow organization theme",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
@ -158,10 +158,7 @@
"Click to Upload": "Click to Upload",
"Client IP": "IP du client",
"Close": "Close",
"Compact": "Compact",
"Created time": "Date de création",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Avatar par défaut",
@ -341,6 +338,7 @@
"Default avatar": "Avatar par défaut",
"Edit Organization": "Modifier l'organisation",
"Favicon": "Favicon",
"Follow global theme": "Follow global theme",
"InitScore": "InitScore",
"Is profile public": "Is profile public",
"Is profile public - Tooltip": "Is profile public - Tooltip",
@ -690,6 +688,19 @@
"Unknown Version": "Unknown Version",
"Version": "Version"
},
"theme": {
"Blossom": "Blossom",
"Border radius": "Border radius",
"Compact": "Compact",
"Customize theme": "Customize theme",
"Dark": "Dark",
"Default": "Default",
"Document": "Document",
"Is compact": "Is compact",
"Primary color": "Primary color",
"Theme": "Theme",
"Theme - Tooltip": "Theme - Tooltip"
},
"token": {
"Access token": "Jeton d'accès",
"Authorization code": "Code d'autorisation",

View File

@ -1,6 +1,5 @@
{
"account": {
"Login": "ログイン",
"Logout": "ログアウト",
"My Account": "マイアカウント",
"Sign Up": "新規登録"
@ -41,6 +40,7 @@
"Enable signup - Tooltip": "Whether to allow users to sign up",
"Failed to sign in": "Failed to sign in",
"File uploaded successfully": "ファイルが正常にアップロードされました",
"Follow organization theme": "Follow organization theme",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
@ -158,10 +158,7 @@
"Click to Upload": "Click to Upload",
"Client IP": "クライアント IP",
"Close": "Close",
"Compact": "Compact",
"Created time": "作成日時",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "デフォルトのアバター",
@ -341,6 +338,7 @@
"Default avatar": "デフォルトのアバター",
"Edit Organization": "組織を編集",
"Favicon": "ファビコン",
"Follow global theme": "Follow global theme",
"InitScore": "InitScore",
"Is profile public": "Is profile public",
"Is profile public - Tooltip": "Is profile public - Tooltip",
@ -690,6 +688,19 @@
"Unknown Version": "Unknown Version",
"Version": "Version"
},
"theme": {
"Blossom": "Blossom",
"Border radius": "Border radius",
"Compact": "Compact",
"Customize theme": "Customize theme",
"Dark": "Dark",
"Default": "Default",
"Document": "Document",
"Is compact": "Is compact",
"Primary color": "Primary color",
"Theme": "Theme",
"Theme - Tooltip": "Theme - Tooltip"
},
"token": {
"Access token": "アクセストークン",
"Authorization code": "認証コード",

View File

@ -1,6 +1,5 @@
{
"account": {
"Login": "Login",
"Logout": "Logout",
"My Account": "My Account",
"Sign Up": "Sign Up"
@ -41,6 +40,7 @@
"Enable signup - Tooltip": "Whether to allow users to sign up",
"Failed to sign in": "Failed to sign in",
"File uploaded successfully": "File uploaded successfully",
"Follow organization theme": "Follow organization theme",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
@ -158,10 +158,7 @@
"Click to Upload": "Click to Upload",
"Client IP": "Client IP",
"Close": "Close",
"Compact": "Compact",
"Created time": "Created time",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Default avatar",
@ -341,6 +338,7 @@
"Default avatar": "Default avatar",
"Edit Organization": "Edit Organization",
"Favicon": "Favicon",
"Follow global theme": "Follow global theme",
"InitScore": "InitScore",
"Is profile public": "Is profile public",
"Is profile public - Tooltip": "Is profile public - Tooltip",
@ -690,6 +688,19 @@
"Unknown Version": "Unknown Version",
"Version": "Version"
},
"theme": {
"Blossom": "Blossom",
"Border radius": "Border radius",
"Compact": "Compact",
"Customize theme": "Customize theme",
"Dark": "Dark",
"Default": "Default",
"Document": "Document",
"Is compact": "Is compact",
"Primary color": "Primary color",
"Theme": "Theme",
"Theme - Tooltip": "Theme - Tooltip"
},
"token": {
"Access token": "Access token",
"Authorization code": "Authorization code",

View File

@ -1,6 +1,5 @@
{
"account": {
"Login": "Логин",
"Logout": "Выйти",
"My Account": "Мой аккаунт",
"Sign Up": "Регистрация"
@ -41,6 +40,7 @@
"Enable signup - Tooltip": "Whether to allow users to sign up",
"Failed to sign in": "Failed to sign in",
"File uploaded successfully": "Файл успешно загружен",
"Follow organization theme": "Follow organization theme",
"Form CSS": "Form CSS",
"Form CSS - Edit": "Form CSS - Edit",
"Form CSS - Tooltip": "Form CSS - Tooltip",
@ -158,10 +158,7 @@
"Click to Upload": "Нажмите здесь, чтобы загрузить",
"Client IP": "IP клиента",
"Close": "Close",
"Compact": "Compact",
"Created time": "Время создания",
"Dark": "Dark",
"Default": "Default",
"Default application": "Default application",
"Default application - Tooltip": "Default application - Tooltip",
"Default avatar": "Аватар по умолчанию",
@ -341,6 +338,7 @@
"Default avatar": "Аватар по умолчанию",
"Edit Organization": "Изменить организацию",
"Favicon": "Иконка",
"Follow global theme": "Follow global theme",
"InitScore": "InitScore",
"Is profile public": "Is profile public",
"Is profile public - Tooltip": "Is profile public - Tooltip",
@ -690,6 +688,19 @@
"Unknown Version": "Unknown Version",
"Version": "Version"
},
"theme": {
"Blossom": "Blossom",
"Border radius": "Border radius",
"Compact": "Compact",
"Customize theme": "Customize theme",
"Dark": "Dark",
"Default": "Default",
"Document": "Document",
"Is compact": "Is compact",
"Primary color": "Primary color",
"Theme": "Theme",
"Theme - Tooltip": "Theme - Tooltip"
},
"token": {
"Access token": "Маркер доступа",
"Authorization code": "Код авторизации",

View File

@ -1,6 +1,5 @@
{
"account": {
"Login": "登录",
"Logout": "登出",
"My Account": "我的账户",
"Sign Up": "注册"
@ -41,6 +40,7 @@
"Enable signup - Tooltip": "是否允许用户注册",
"Failed to sign in": "登录失败",
"File uploaded successfully": "文件上传成功",
"Follow organization theme": "使用组织主题",
"Form CSS": "表单CSS",
"Form CSS - Edit": "编辑表单CSS",
"Form CSS - Tooltip": "表单的CSS样式如增加边框和阴影",
@ -158,10 +158,7 @@
"Click to Upload": "点击上传",
"Client IP": "客户端IP",
"Close": "关闭",
"Compact": "紧凑",
"Created time": "创建时间",
"Dark": "黑暗",
"Default": "默认",
"Default application": "默认应用",
"Default application - Tooltip": "默认应用",
"Default avatar": "默认头像",
@ -341,6 +338,7 @@
"Default avatar": "默认头像",
"Edit Organization": "编辑组织",
"Favicon": "图标",
"Follow global theme": "使用全局默认主题",
"InitScore": "初始积分",
"Is profile public": "用户个人页公开",
"Is profile public - Tooltip": "关闭后,只有全局管理员或同组织用户才能访问用户主页",
@ -690,6 +688,19 @@
"Unknown Version": "未知版本",
"Version": "版本"
},
"theme": {
"Blossom": "桃花缘",
"Border radius": "圆角",
"Compact": "紧凑",
"Customize theme": "定制主题",
"Dark": "暗黑",
"Default": "默认",
"Document": "知识协作",
"Is compact": "宽松度",
"Primary color": "主色",
"Theme": "主题",
"Theme - Tooltip": "为你的应用设置主题"
},
"token": {
"Access token": "访问令牌",
"Authorization code": "授权码",

File diff suppressed because it is too large Load Diff