Skip to content

Commit 4bcbc11

Browse files
committed
Merge branch 'dev' of github.com:lowcoder-org/lowcoder into dev
2 parents 233c56a + f2400d2 commit 4bcbc11

File tree

8 files changed

+209
-70
lines changed

8 files changed

+209
-70
lines changed

client/packages/lowcoder/src/api/inviteApi.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type InviteInfo = {
2020
class InviteApi extends Api {
2121
static getInviteURL = "/invitation";
2222
static acceptInviteURL = (invitationId: string) => `/invitation/${invitationId}/invite`;
23+
static sendInvitationURL = `${this.getInviteURL}/email/invite`;
2324

2425
// generate invitation
2526
static getInvite(request: GetInviteRequest): AxiosPromise<GenericApiResponse<InviteInfo>> {
@@ -36,6 +37,11 @@ class InviteApi extends Api {
3637
// the same api as getInviteInfo, method is by post
3738
return Api.get(InviteApi.acceptInviteURL(request.invitationId));
3839
}
40+
41+
// send invitations
42+
static sendInvitations(request: {emails: string[], orgId: string}): AxiosPromise<GenericApiResponse<any>> {
43+
return Api.post(InviteApi.sendInvitationURL, request);
44+
}
3945
}
4046

4147
export default InviteApi;

client/packages/lowcoder/src/i18n/locales/en.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2484,6 +2484,12 @@ export const en = {
24842484
"inviteUserLabel": "Invitation Link:",
24852485
"inviteCopyLink": "Copy Link",
24862486
"inviteText": "{userName} Invites You to Join the Workspace \"{organization}\", Click on the Link to Join: {inviteLink}",
2487+
"inviteByEmailHelp": "You can enter one or more email addresses to send invitation links",
2488+
"inviteByEmailLabel": "Enter emails:",
2489+
"inviteByEmailButton": "Send Invitations",
2490+
"inviteByEmailSuccess": "Invitations sent successfully!",
2491+
"inviteByEmailError": "Something went wrong while sending invitations. Please try again.",
2492+
"noValidEmails": "No valid emails found",
24872493
"groupName": "Group Name",
24882494
"createTime": "Create Time",
24892495
"manageBtn": "Manage",
@@ -3079,6 +3085,7 @@ export const en = {
30793085
"memberOfOrgs": "Workspaces Membership",
30803086
"apiKeys": "API Keys",
30813087
"createApiKey": "Create API Key",
3088+
"apiKeyInfo": "Make sure to copy your new API key now. You won't be able to see it again.",
30823089
"apiKeyName": "Name",
30833090
"apiKeyDescription": "Description",
30843091
"apiKeyCopy": "Click the Api Key to get the value in your clipboard",

client/packages/lowcoder/src/pages/ApplicationV2/components/CreateApiKeyModal.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { validateResponse } from "api/apiUtils";
1313
import _ from "lodash";
1414
import { styled } from "styled-components";
1515
import UserApi, { ApiKeyPayload } from "api/userApi";
16+
import { ApiKeyType } from "./UserApiKeysCard";
1617

1718
const CustomModalStyled = styled(CustomModal)`
1819
button {
@@ -90,7 +91,7 @@ const FormStyled = styled(Form)`
9091
type CreateApiKeyModalProps = {
9192
modalVisible: boolean;
9293
closeModal: () => void;
93-
onConfigCreate: () => void;
94+
onConfigCreate: (apiKey?: ApiKeyType) => void;
9495
};
9596

9697
function CreateApiKeyModal(props: CreateApiKeyModalProps) {
@@ -101,6 +102,7 @@ function CreateApiKeyModal(props: CreateApiKeyModalProps) {
101102
} = props;
102103
const [form] = Form.useForm();
103104
const [saveLoading, setSaveLoading] = useState(false);
105+
const [apiKey, setApiKey] = useState<{id: string, token: string}>();
104106

105107
const handleOk = () => {
106108
form.validateFields().then(values => {
@@ -115,12 +117,15 @@ function CreateApiKeyModal(props: CreateApiKeyModalProps) {
115117
.then((resp) => {
116118
if (validateResponse(resp)) {
117119
messageInstance.success(trans("idSource.saveSuccess"));
120+
onConfigCreate(resp.data.data);
118121
}
119122
})
120-
.catch((e) => messageInstance.error(e.message))
123+
.catch((e) => {
124+
messageInstance.error(e.message);
125+
onConfigCreate();
126+
})
121127
.finally(() => {
122128
setSaveLoading(false);
123-
onConfigCreate();
124129
});
125130
}
126131

client/packages/lowcoder/src/pages/ApplicationV2/components/UserApiKeysCard.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import CreateApiKeyModal from "./CreateApiKeyModal";
1414
import { fetchApiKeysAction } from "redux/reduxActions/userActions";
1515
import UserApi from "@lowcoder-ee/api/userApi";
1616
import { validateResponse } from "@lowcoder-ee/api/apiUtils";
17+
import Alert from "antd/es/alert";
18+
import { CopyOutlined } from "@ant-design/icons";
1719

1820
const TableStyled = styled(Table)`
1921
.ant-table-tbody > tr > td {
@@ -37,10 +39,16 @@ const CreateButton = styled(TacoButton)`
3739
box-shadow: none;
3840
`;
3941

42+
export type ApiKeyType = {
43+
id: string;
44+
token: string;
45+
}
46+
4047
export default function UserApiKeysCard() {
4148
const dispatch = useDispatch();
4249
const apiKeys = useSelector(getApiKeys);
4350
const [modalVisible, setModalVisible] = useState(false);
51+
const [newApiKey, setNewApiKey] = useState<ApiKeyType>();
4452

4553
const handleCopy = (value: string) => {
4654
navigator.clipboard.writeText(value).then(() => {
@@ -66,13 +74,11 @@ export default function UserApiKeysCard() {
6674
{trans("profile.createApiKey")}
6775
</CreateButton>
6876
</Flex>
77+
{Boolean(newApiKey) && <Alert message={trans("profile.apiKeyInfo")} type="info" style={{marginBottom: '16px'}}/>}
6978
<TableStyled
7079
tableLayout={"auto"}
7180
scroll={{ x: "100%" }}
7281
pagination={false}
73-
onRow={(record) => ({
74-
75-
})}
7682
columns={[
7783
{
7884
title: trans("profile.apiKeyName"),
@@ -95,16 +101,19 @@ export default function UserApiKeysCard() {
95101
title: trans("profile.apiKey"),
96102
dataIndex: "token",
97103
ellipsis: true,
98-
render: (value: string) => {
99-
const startToken = value.substring(0, 6);
100-
const endToken = value.substring(value.length - 6);
101-
return (
102-
<Tooltip placement="topLeft" title={ trans("profile.apiKeyCopy")}>
103-
<div onClick={() => handleCopy(value)} style={{ cursor: 'pointer' }}>
104-
{`${startToken}********************${endToken}`}
105-
</div>
106-
</Tooltip>
107-
)
104+
render: (value: string, record: any) => {
105+
if (newApiKey?.id === record.id) {
106+
return (
107+
<Tooltip placement="topLeft" title={ trans("profile.apiKeyCopy")}>
108+
<div onClick={() => handleCopy(newApiKey?.token!)} style={{ cursor: 'pointer' }}>
109+
{value}
110+
&nbsp;
111+
<CopyOutlined />
112+
</div>
113+
</Tooltip>
114+
)
115+
}
116+
return <div>{value}</div>
108117
}
109118
},
110119
{ title: " ", dataIndex: "operation", width: "208px" },
@@ -145,8 +154,9 @@ export default function UserApiKeysCard() {
145154
<CreateApiKeyModal
146155
modalVisible={modalVisible}
147156
closeModal={() => setModalVisible(false)}
148-
onConfigCreate={() => {
157+
onConfigCreate={(apiKey?: ApiKeyType) => {
149158
setModalVisible(false);
159+
setNewApiKey(apiKey);
150160
dispatch(fetchApiKeysAction());
151161
}}
152162
/>

client/packages/lowcoder/src/pages/common/inviteDialog.tsx

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import { HelpText } from "components/HelpText";
1212
import copyToClipboard from "copy-to-clipboard";
1313
import { trans } from "i18n";
1414
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
15+
import Divider from "antd/es/divider";
16+
import Flex from "antd/es/flex";
17+
import Select from "antd/es/select";
1518

1619
const InviteButton = styled(TacoButton)`
1720
width: 76px;
@@ -23,14 +26,36 @@ const StyledLoading = styled(WhiteLoading)`
2326
height: 170px;
2427
`;
2528

26-
function InviteContent(props: { inviteInfo: InviteInfo }) {
27-
const { inviteInfo } = props;
29+
function InviteContent(props: { inviteInfo: InviteInfo, onClose?: () => void }) {
30+
const { inviteInfo, onClose } = props;
2831
const inviteLink = genInviteLink(inviteInfo?.inviteCode);
2932
const inviteText = trans("memberSettings.inviteText", {
3033
userName: inviteInfo.createUserName,
3134
organization: inviteInfo.invitedOrganizationName,
3235
inviteLink,
3336
});
37+
const [emails, setEmails] = useState<string[]>([]);
38+
39+
const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
40+
41+
const sendInvitations = async () => {
42+
const filteredEmails = emails.filter(isValidEmail);
43+
if (!filteredEmails.length) {
44+
return messageInstance.error(trans("memberSettings.noValidEmails"));
45+
}
46+
try {
47+
const resp = await InviteApi.sendInvitations({emails: filteredEmails, orgId: inviteInfo.invitedOrganizationId})
48+
if (validateResponse(resp) && resp.data.success) {
49+
messageInstance.success(trans('membersSettings.inviteByEmailSuccess'));
50+
onClose?.();
51+
return;
52+
}
53+
throw new Error(trans('membersSettings.inviteByEmailError'));
54+
} catch(e: any) {
55+
messageInstance.error(e.message);
56+
}
57+
}
58+
3459
return (
3560
<>
3661
<HelpText style={{ marginBottom: 16 }}>{trans("memberSettings.inviteUserHelp")}</HelpText>
@@ -48,6 +73,34 @@ function InviteContent(props: { inviteInfo: InviteInfo }) {
4873
{trans("memberSettings.inviteCopyLink")}
4974
</InviteButton>
5075
</div>
76+
<Divider style={{marginTop: '60px'}}/>
77+
<HelpText style={{ marginBottom: 16 }}>{trans("memberSettings.inviteByEmailHelp")}</HelpText>
78+
<CommonTextLabel>{trans("memberSettings.inviteByEmailLabel")}</CommonTextLabel>
79+
<Select
80+
mode="tags"
81+
allowClear
82+
open={false}
83+
style={{ width: '100%', marginTop: '8px', marginBottom: '8px' }}
84+
placeholder="Enter emails"
85+
defaultValue={[]}
86+
onChange={(value) => {
87+
setEmails(value);
88+
}}
89+
options={[]}
90+
showSearch={false}
91+
suffixIcon={''}
92+
/>
93+
<Flex justify="end">
94+
<TacoButton
95+
buttonType="primary"
96+
onClick={() => {
97+
sendInvitations();
98+
}}
99+
disabled={!Boolean(emails?.length)}
100+
>
101+
{trans("memberSettings.inviteByEmailButton")}
102+
</TacoButton>
103+
</Flex>
51104
</>
52105
);
53106
}
@@ -101,7 +154,7 @@ function InviteDialog(props: {
101154
showCancelButton={false}
102155
width="440px"
103156
>
104-
{!inviteInfo ? <StyledLoading size={20} /> : <InviteContent inviteInfo={inviteInfo} />}
157+
{!inviteInfo ? <StyledLoading size={20} /> : <InviteContent inviteInfo={inviteInfo} onClose={() => setInviteDialogVisible(false)} />}
105158
</CustomModal>
106159
</>
107160
);

client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export default function FormLoginSteps(props: FormLoginProps) {
127127
const isEmailLoginEnabled = useMemo(() => {
128128
return isFormLoginEnabled && signinEnabled;
129129
}, [isFormLoginEnabled, signinEnabled]);
130-
130+
131131
const isEnterpriseMode = useMemo(() => {
132132
return serverSettings?.LOWCODER_WORKSPACE_MODE === "ENTERPRISE" || serverSettings?.LOWCODER_WORKSPACE_MODE === "SINGLEWORKSPACE";
133133
}, [serverSettings]);
@@ -204,7 +204,7 @@ export default function FormLoginSteps(props: FormLoginProps) {
204204
}
205205
if (resp.data.length === 1) {
206206
// in Enterprise mode, we will get org data in different format
207-
const selectedOrgId = isEnterpriseMode ? resp.data[0].id : resp.data[0].orgId;
207+
const selectedOrgId = resp.data[0]?.id || resp.data[0]?.orgId;
208208
setOrganizationId(selectedOrgId);
209209
dispatch(fetchConfigAction(selectedOrgId));
210210
setCurrentStep(CurrentStepEnum.AUTH_PROVIDERS);
@@ -239,7 +239,7 @@ export default function FormLoginSteps(props: FormLoginProps) {
239239
invitedOrganizationId={organizationId}
240240
authGoal="login"
241241
/>
242-
{signupEnabled && (
242+
{(isEmailLoginEnabled && signupEnabled) && (
243243
<>
244244
<Divider/>
245245
<AuthBottomView>

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy