Compare commits

...

11 Commits

Author SHA1 Message Date
Yang Luo
bf730050d5 feat: increase Organization.Favicon to 200 chars 2024-09-29 11:45:56 +08:00
Yang Luo
5b733b7f15 feat: improve filterRecordIn24Hours() logic 2024-09-29 11:45:15 +08:00
ZhaoYP 2001
034f28def9 feat: logout if app.conf's inactiveTimeoutMinutes is reached (#3244)
* feat: logout if there's no activities for a long time

* fix: change the implementation of updating LastTime

* fix: add logoutMinites to app.conf

* fix: change the implementation of judgment statement

* fix: use sync.Map to ensure thread safety

* fix: syntax standards and Apache headers

* fix: change the implementation of obtaining logoutMinutes in app.conf

* fix: follow community code standards

* fix: <=0 or empty means no restriction

* Update logout_filter.go

* Update app.conf

* Update main.go

* Update and rename logout_filter.go to timeout_filter.go

* Update app.conf

* Update timeout_filter.go

* fix: update app.conf

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-09-27 01:18:02 +08:00
DacongDA
c86ac8e6ad feat: fix UTF-8 charset for Alipay IdP (#3247) 2024-09-27 00:59:52 +08:00
Jack Merrill
d647eed22a feat: add OIDC WebFinger support (#3245)
* feat: add WebFinger support

* lint: used gofumpt

* oidc: ensure webfinger rel is checked
2024-09-26 13:06:36 +08:00
Yang Luo
717c53f6e5 feat: support enableErrorMask2 config 2024-09-25 19:37:14 +08:00
千石
097adac871 feat: support single-choice and multi-choices in signup page (#3234)
* feat: add custom signup field

* feat: support more field in signup page

* feat: support more field in signup page

* feat: support more field in signup page

* feat: Reduce code duplication in form item rendering

* feat: Simplify gender and info checks using includes

* feat: update translate

* Revert "feat: update translate"

This reverts commit 669334c716.

* feat: address feedback from hsluoyz
2024-09-25 12:48:37 +08:00
IZUMI-Zu
74543b9533 feat: improve QR code for casdoor-app (#3226)
* feat: simplify login url for casdoor-app

* feat: add token check

* fix: improve logic
2024-09-23 22:27:58 +08:00
Yang Luo
110dc04179 feat: Revert "feat: fix permission problem in standard image" (#3231)
This reverts commit 6464bd10dc.
2024-09-23 22:19:27 +08:00
DacongDA
6464bd10dc feat: fix permission problem in standard image (#3228) 2024-09-23 18:40:39 +08:00
Yang Luo
db878a890e feat: add type and options to signup items 2024-09-21 23:40:29 +08:00
19 changed files with 395 additions and 42 deletions

View File

@@ -77,6 +77,7 @@ p, *, *, POST, /api/verify-code, *, *
p, *, *, POST, /api/reset-email-or-phone, *, *
p, *, *, POST, /api/upload-resource, *, *
p, *, *, GET, /.well-known/openid-configuration, *, *
p, *, *, GET, /.well-known/webfinger, *, *
p, *, *, *, /.well-known/jwks, *, *
p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, *

View File

@@ -23,6 +23,7 @@ isDemoMode = false
batchSize = 100
enableErrorMask = false
enableGzip = true
inactiveTimeoutMinutes =
ldapServerPort = 389
radiusServerPort = 1812
radiusSecret = "secret"

View File

@@ -200,6 +200,10 @@ func (c *ApiController) Signup() {
Type: userType,
Password: authForm.Password,
DisplayName: authForm.Name,
Gender: authForm.Gender,
Bio: authForm.Bio,
Tag: authForm.Tag,
Education: authForm.Education,
Avatar: organization.DefaultAvatar,
Email: authForm.Email,
Phone: authForm.Phone,

View File

@@ -14,7 +14,11 @@
package controllers
import "github.com/casdoor/casdoor/object"
import (
"strings"
"github.com/casdoor/casdoor/object"
)
// GetOidcDiscovery
// @Title GetOidcDiscovery
@@ -42,3 +46,31 @@ func (c *RootController) GetJwks() {
c.Data["json"] = jwks
c.ServeJSON()
}
// GetWebFinger
// @Title GetWebFinger
// @Tag OIDC API
// @Param resource query string true "resource"
// @Success 200 {object} object.WebFinger
// @router /.well-known/webfinger [get]
func (c *RootController) GetWebFinger() {
resource := c.Input().Get("resource")
rels := []string{}
host := c.Ctx.Request.Host
for key, value := range c.Input() {
if strings.HasPrefix(key, "rel") {
rels = append(rels, value...)
}
}
webfinger, err := object.GetWebFinger(resource, rels, host)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = webfinger
c.Ctx.Output.ContentType("application/jrd+json")
c.ServeJSON()
}

View File

@@ -410,6 +410,12 @@ func (c *ApiController) GetEmailAndPhone() {
organization := c.Ctx.Request.Form.Get("organization")
username := c.Ctx.Request.Form.Get("username")
enableErrorMask2 := conf.GetConfigBool("enableErrorMask2")
if enableErrorMask2 {
c.ResponseError("Error")
return
}
user, err := object.GetUserByFields(organization, username)
if err != nil {
c.ResponseError(err.Error())

View File

@@ -45,6 +45,15 @@ func (c *ApiController) ResponseOk(data ...interface{}) {
// ResponseError ...
func (c *ApiController) ResponseError(error string, data ...interface{}) {
enableErrorMask2 := conf.GetConfigBool("enableErrorMask2")
if enableErrorMask2 {
error = c.T("subscription:Error")
resp := &Response{Status: "error", Msg: error}
c.ResponseJsonData(resp, data...)
return
}
enableErrorMask := conf.GetConfigBool("enableErrorMask")
if enableErrorMask {
if strings.HasPrefix(error, "The user: ") && strings.HasSuffix(error, " doesn't exist") || strings.HasPrefix(error, "用户: ") && strings.HasSuffix(error, "不存在") {

View File

@@ -26,6 +26,10 @@ type AuthForm struct {
Name string `json:"name"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Gender string `json:"gender"`
Bio string `json:"bio"`
Tag string `json:"tag"`
Education string `json:"education"`
Email string `json:"email"`
Phone string `json:"phone"`
Affiliation string `json:"affiliation"`

View File

@@ -200,7 +200,7 @@ func (idp *AlipayIdProvider) postWithBody(body interface{}, targetUrl string) ([
formData.Set("sign", sign)
resp, err := idp.Client.PostForm(targetUrl, formData)
resp, err := idp.Client.Post(targetUrl, "application/x-www-form-urlencoded;charset=utf-8", strings.NewReader(formData.Encode()))
if err != nil {
return nil, err
}

View File

@@ -56,6 +56,7 @@ func main() {
beego.InsertFilter("*", beego.BeforeRouter, routers.StaticFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.AutoSigninFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.CorsFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.TimeoutFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.ApiFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.PrometheusFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)

View File

@@ -31,15 +31,17 @@ type SigninMethod struct {
}
type SignupItem struct {
Name string `json:"name"`
Visible bool `json:"visible"`
Required bool `json:"required"`
Prompted bool `json:"prompted"`
CustomCss string `json:"customCss"`
Label string `json:"label"`
Placeholder string `json:"placeholder"`
Regex string `json:"regex"`
Rule string `json:"rule"`
Name string `json:"name"`
Visible bool `json:"visible"`
Required bool `json:"required"`
Prompted bool `json:"prompted"`
Type string `json:"type"`
CustomCss string `json:"customCss"`
Label string `json:"label"`
Placeholder string `json:"placeholder"`
Options []string `json:"options"`
Regex string `json:"regex"`
Rule string `json:"rule"`
}
type SigninItem struct {
@@ -85,7 +87,7 @@ type Application struct {
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SigninMethods []*SigninMethod `xorm:"varchar(2000)" json:"signinMethods"`
SignupItems []*SignupItem `xorm:"varchar(2000)" json:"signupItems"`
SignupItems []*SignupItem `xorm:"varchar(3000)" json:"signupItems"`
SigninItems []*SigninItem `xorm:"mediumtext" json:"signinItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"`

View File

@@ -44,6 +44,18 @@ type OidcDiscovery struct {
EndSessionEndpoint string `json:"end_session_endpoint"`
}
type WebFinger struct {
Subject string `json:"subject"`
Links []WebFingerLink `json:"links"`
Aliases *[]string `json:"aliases,omitempty"`
Properties *map[string]string `json:"properties,omitempty"`
}
type WebFingerLink struct {
Rel string `json:"rel"`
Href string `json:"href"`
}
func isIpAddress(host string) bool {
// Attempt to split the host and port, ignoring the error
hostWithoutPort, _, err := net.SplitHostPort(host)
@@ -160,3 +172,43 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
return jwks, nil
}
func GetWebFinger(resource string, rels []string, host string) (WebFinger, error) {
wf := WebFinger{}
resourceSplit := strings.Split(resource, ":")
if len(resourceSplit) != 2 {
return wf, fmt.Errorf("invalid resource")
}
resourceType := resourceSplit[0]
resourceValue := resourceSplit[1]
oidcDiscovery := GetOidcDiscovery(host)
switch resourceType {
case "acct":
user, err := GetUserByEmailOnly(resourceValue)
if err != nil {
return wf, err
}
if user == nil {
return wf, fmt.Errorf("user not found")
}
wf.Subject = resource
for _, rel := range rels {
if rel == "http://openid.net/specs/connect/1.0/issuer" {
wf.Links = append(wf.Links, WebFingerLink{
Rel: "http://openid.net/specs/connect/1.0/issuer",
Href: oidcDiscovery.Issuer,
})
}
}
}
return wf, nil
}

View File

@@ -56,7 +56,7 @@ type Organization struct {
WebsiteUrl string `xorm:"varchar(100)" json:"websiteUrl"`
Logo string `xorm:"varchar(200)" json:"logo"`
LogoDark string `xorm:"varchar(200)" json:"logoDark"`
Favicon string `xorm:"varchar(100)" json:"favicon"`
Favicon string `xorm:"varchar(200)" json:"favicon"`
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`

View File

@@ -166,19 +166,76 @@ func AddToVerificationRecord(user *User, provider *Provider, remoteAddr, recordT
return nil
}
func filterRecordIn24Hours(record *VerificationRecord) *VerificationRecord {
if record == nil {
return nil
}
now := time.Now().Unix()
if now-record.Time > 60*60*24 {
return nil
}
return record
}
func getVerificationRecord(dest string) (*VerificationRecord, error) {
var record VerificationRecord
record := &VerificationRecord{}
record.Receiver = dest
has, err := ormer.Engine.Desc("time").Where("is_used = false").Get(&record)
has, err := ormer.Engine.Desc("time").Where("is_used = false").Get(record)
if err != nil {
return nil, err
}
record = filterRecordIn24Hours(record)
if record == nil {
has = false
}
if !has {
record = &VerificationRecord{}
record.Receiver = dest
has, err = ormer.Engine.Desc("time").Get(record)
if err != nil {
return nil, err
}
record = filterRecordIn24Hours(record)
if record == nil {
has = false
}
if !has {
return nil, nil
}
return record, nil
}
return record, nil
}
func getUnusedVerificationRecord(dest string) (*VerificationRecord, error) {
record := &VerificationRecord{}
record.Receiver = dest
has, err := ormer.Engine.Desc("time").Where("is_used = false").Get(record)
if err != nil {
return nil, err
}
record = filterRecordIn24Hours(record)
if record == nil {
has = false
}
if !has {
return nil, nil
}
return &record, nil
return record, nil
}
func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult, error) {
@@ -187,7 +244,9 @@ func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult
return nil, err
}
if record == nil {
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet, or has already been used!")}, nil
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet!")}, nil
} else if record.IsUsed {
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has already been used!")}, nil
}
timeoutInMinutes, err := conf.GetConfigInt64("verificationCodeTimeout")
@@ -196,9 +255,6 @@ func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult
}
now := time.Now().Unix()
if now-record.Time > timeoutInMinutes*60*10 {
return &VerifyResult{noRecordError, i18n.Translate(lang, "verification:The verification code has not been sent yet!")}, nil
}
if now-record.Time > timeoutInMinutes*60 {
return &VerifyResult{timeoutError, fmt.Sprintf(i18n.Translate(lang, "verification:You should verify your code in %d min!"), timeoutInMinutes)}, nil
}
@@ -211,7 +267,7 @@ func CheckVerificationCode(dest string, code string, lang string) (*VerifyResult
}
func DisableVerificationCode(dest string) error {
record, err := getVerificationRecord(dest)
record, err := getUnusedVerificationRecord(dest)
if record == nil || err != nil {
return nil
}

View File

@@ -290,6 +290,7 @@ func initAPI() {
beego.Router("/.well-known/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscovery")
beego.Router("/.well-known/jwks", &controllers.RootController{}, "*:GetJwks")
beego.Router("/.well-known/webfinger", &controllers.RootController{}, "GET:GetWebFinger")
beego.Router("/cas/:organization/:application/serviceValidate", &controllers.RootController{}, "GET:CasServiceValidate")
beego.Router("/cas/:organization/:application/proxyValidate", &controllers.RootController{}, "GET:CasProxyValidate")

64
routers/timeout_filter.go Normal file
View File

@@ -0,0 +1,64 @@
// Copyright 2024 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.
package routers
import (
"fmt"
"sync"
"time"
"github.com/beego/beego/context"
"github.com/casdoor/casdoor/conf"
)
var (
inactiveTimeoutMinutes int64
requestTimeMap sync.Map
)
func init() {
var err error
inactiveTimeoutMinutes, err = conf.GetConfigInt64("inactiveTimeoutMinutes")
if err != nil {
inactiveTimeoutMinutes = 0
}
}
func timeoutLogout(ctx *context.Context, sessionId string) {
requestTimeMap.Delete(sessionId)
ctx.Input.CruSession.Set("username", "")
ctx.Input.CruSession.Set("accessToken", "")
ctx.Input.CruSession.Delete("SessionData")
responseError(ctx, fmt.Sprintf(T(ctx, "auth:Timeout for inactivity of %d minutes"), inactiveTimeoutMinutes))
}
func TimeoutFilter(ctx *context.Context) {
if inactiveTimeoutMinutes <= 0 {
return
}
owner, name := getSubject(ctx)
if owner == "anonymous" || name == "anonymous" {
return
}
sessionId := ctx.Input.CruSession.SessionID()
currentTime := time.Now()
preRequestTime, has := requestTimeMap.Load(sessionId)
requestTimeMap.Store(sessionId, currentTime)
if has && preRequestTime.(time.Time).Add(time.Minute*time.Duration(inactiveTimeoutMinutes)).Before(currentTime) {
timeoutLogout(ctx, sessionId)
}
}

View File

@@ -1050,12 +1050,7 @@ class UserEditPage extends React.Component {
<MfaAccountTable
title={i18next.t("user:MFA accounts")}
table={this.state.user.mfaAccounts}
qrUrl={
"casdoor-app://login/into?serverUrl=" + window.location.origin +
"&clientId=" + this.state.application.clientId +
"&organizationName=" + this.state.organizationName +
"&appName=" + this.state.user.signupApplication
}
accessToken={this.props.account?.accessToken}
icon={this.state.user.avatar}
onUpdateTable={(table) => {this.updateUserField("mfaAccounts", table);}}
/>

View File

@@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Form, Input, Radio, Result, Row, message} from "antd";
import {Button, Form, Input, Radio, Result, Row, Select, message} from "antd";
import * as Setting from "../Setting";
import * as AuthBackend from "./AuthBackend";
import * as ProviderButton from "./ProviderButton";
@@ -50,6 +50,38 @@ const formItemLayout = {
},
};
const renderFormItem = (signupItem) => {
const commonProps = {
name: signupItem.name.toLowerCase(),
label: signupItem.label || signupItem.name,
rules: [
{
required: signupItem.required,
message: i18next.t(`signup:Please input your ${signupItem.label || signupItem.name}!`),
},
],
};
if (!signupItem.type || signupItem.type === "Input") {
return (
<Form.Item {...commonProps}>
<Input placeholder={signupItem.placeholder} />
</Form.Item>
);
} else if (signupItem.type === "Single Choice" || signupItem.type === "Multiple Choices") {
return (
<Form.Item {...commonProps}>
<Select
mode={signupItem.type === "Multiple Choices" ? "multiple" : "single"}
placeholder={signupItem.placeholder}
showSearch={false}
options={signupItem.options.map(option => ({label: option, value: option}))}
/>
</Form.Item>
);
}
};
export const tailFormItemLayout = {
wrapperCol: {
xs: {
@@ -198,6 +230,22 @@ class SignupPage extends React.Component {
onFinish(values) {
const application = this.getApplicationObj();
if (Array.isArray(values.gender)) {
values.gender = values.gender.join(", ");
}
if (Array.isArray(values.bio)) {
values.bio = values.bio.join(", ");
}
if (Array.isArray(values.tag)) {
values.tag = values.tag.join(", ");
}
if (Array.isArray(values.education)) {
values.education = values.education.join(", ");
}
const params = new URLSearchParams(window.location.search);
values.plan = params.get("plan");
values.pricing = params.get("pricing");
@@ -238,6 +286,7 @@ class SignupPage extends React.Component {
}
renderFormItem(application, signupItem) {
const validItems = ["Gender", "Bio", "Tag", "Education"];
if (!signupItem.visible) {
return null;
}
@@ -366,7 +415,9 @@ class SignupPage extends React.Component {
},
]}
>
<RegionSelect className="signup-region-select" onChange={(value) => {this.setState({region: value});}} />
<RegionSelect className="signup-region-select" onChange={(value) => {
this.setState({region: value});
}} />
</Form.Item>
);
} else if (signupItem.name === "Email" || signupItem.name === "Phone" || signupItem.name === "Email or Phone" || signupItem.name === "Phone or Email") {
@@ -669,8 +720,9 @@ class SignupPage extends React.Component {
</span>
);
})
);
} else if (validItems.includes(signupItem.name)) {
return renderFormItem(signupItem);
}
}

View File

@@ -14,7 +14,7 @@
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Image, Input, Popover, QRCode, Row, Table, Tooltip} from "antd";
import {Alert, Button, Col, Image, Input, Popover, QRCode, Row, Table, Tooltip} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
@@ -23,7 +23,6 @@ class MfaAccountTable extends React.Component {
super(props);
this.state = {
classes: props,
qrUrl: this.props.qrUrl,
icon: this.props.icon,
mfaAccounts: this.props.table !== null ? this.props.table.map((item, index) => {
item.key = index;
@@ -77,6 +76,42 @@ class MfaAccountTable extends React.Component {
this.updateTable(table);
}
getQrUrl() {
const {accessToken} = this.props;
let qrUrl = `casdoor-app://login?serverUrl=${window.location.origin}&accessToken=${accessToken}`;
let error = null;
if (!accessToken) {
qrUrl = "";
error = i18next.t("general:Access token is empty");
}
if (qrUrl.length >= 2000) {
qrUrl = "";
error = i18next.t("general:QR code is too large");
}
return {qrUrl, error};
}
renderQrCode() {
const {qrUrl, error} = this.getQrUrl();
if (error) {
return <Alert message={error} type="error" showIcon />;
} else {
return (
<QRCode
value={qrUrl}
icon={this.state.icon}
errorLevel="M"
size={230}
bordered={false}
/>
);
}
}
renderTable(table) {
const columns = [
{
@@ -159,14 +194,9 @@ class MfaAccountTable extends React.Component {
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
<Popover trigger="focus" content={
<QRCode
value={this.state.qrUrl}
icon={this.state.icon}
bordered={false}
/>
}>
<Button style={{marginRight: "10px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
<Popover trigger="focus" overlayInnerStyle={{padding: 0}}
content={this.renderQrCode()}>
<Button style={{marginLeft: "5px"}} type="primary" size="small">{i18next.t("general:QR Code")}</Button>
</Popover>
</div>

View File

@@ -65,7 +65,7 @@ class SignupTable extends React.Component {
}
addRow(table) {
const row = {name: Setting.getNewRowNameForTable(table, "Please select a signup item"), visible: true, required: true, rule: "None", customCss: ""};
const row = {name: Setting.getNewRowNameForTable(table, "Please select a signup item"), visible: true, required: true, options: [], rule: "None", customCss: ""};
if (table === undefined) {
table = [];
}
@@ -100,6 +100,10 @@ class SignupTable extends React.Component {
{name: "ID", displayName: i18next.t("general:ID")},
{name: "Display name", displayName: i18next.t("general:Display name")},
{name: "Affiliation", displayName: i18next.t("user:Affiliation")},
{name: "Gender", displayName: i18next.t("user:Gender")},
{name: "Bio", displayName: i18next.t("user:Bio")},
{name: "Tag", displayName: i18next.t("user:Tag")},
{name: "Education", displayName: i18next.t("user:Education")},
{name: "Country/Region", displayName: i18next.t("user:Country/Region")},
{name: "ID card", displayName: i18next.t("user:ID card")},
{name: "Password", displayName: i18next.t("general:Password")},
@@ -201,6 +205,25 @@ class SignupTable extends React.Component {
);
},
},
{
title: i18next.t("provider:Type"),
dataIndex: "type",
key: "type",
width: "160px",
render: (text, record, index) => {
const options = [
{id: "Input", name: i18next.t("application:Input")},
{id: "Single Choice", name: i18next.t("application:Single Choice")},
{id: "Multiple Choices", name: i18next.t("application:Multiple Choices")},
];
return (
<Select virtual={false} style={{width: "100%"}} value={text} onChange={(value => {
this.updateField(table, index, "type", value);
})} options={options.map(item => Setting.getOption(item.name, item.id))} />
);
},
},
{
title: i18next.t("signup:Label"),
dataIndex: "label",
@@ -261,7 +284,7 @@ class SignupTable extends React.Component {
title: i18next.t("signup:Placeholder"),
dataIndex: "placeholder",
key: "placeholder",
width: "200px",
width: "110px",
render: (text, record, index) => {
if (record.name.startsWith("Text ")) {
return null;
@@ -274,6 +297,26 @@ class SignupTable extends React.Component {
);
},
},
{
title: i18next.t("signup:Options"),
dataIndex: "options",
key: "options",
width: "180px",
render: (text, record, index) => {
if (record.type !== "Single Choice" && record.type !== "Multiple Choices") {
return null;
}
return (
<Select virtual={false} mode="tags" style={{width: "100%"}} value={text}
onChange={(value => {
this.updateField(table, index, "options", value);
})}
options={text?.map((option) => Setting.getOption(option, option))}
/>
);
},
},
{
title: i18next.t("signup:Regex"),
dataIndex: "regex",