Skip to content

Commit 69d75d6

Browse files
authored
Merge branch 'dev' into fix_org_by_email
2 parents 4f36ff4 + 2b36ad3 commit 69d75d6

File tree

16 files changed

+259
-80
lines changed

16 files changed

+259
-80
lines changed

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

+6
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/components/layout/Layout.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,10 @@ export function Layout(props: LayoutProps) {
111111
placement="right"
112112
closable={true}
113113
onClose={toggleDrawer}
114-
visible={drawerVisible}
115-
bodyStyle={{ padding: "0px" }}
114+
open={drawerVisible}
115+
styles={{
116+
body: { padding: "0px" }
117+
}}
116118
destroyOnClose // Ensure drawer content is removed when closed
117119
>
118120
<DrawerContentWrapper>

client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ export function ListView(props: Props) {
369369
newData.splice(toIndex, 0, movedItem);
370370

371371
children.listData.dispatchChangeValueAction(newData);
372+
children.onEvent.getView()('sortChange');
372373
};
373374

374375
// log.debug("renders: ", renders);

client/packages/lowcoder/src/comps/comps/listViewComp/listViewComp.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
withFunction,
3030
WrapContextNodeV2,
3131
} from "lowcoder-core";
32-
import { JSONArray, JSONValue } from "util/jsonTypes";
32+
import { JSONArray, JSONObject, JSONValue } from "util/jsonTypes";
3333
import { depthEqual, lastValueIfEqual, shallowEqual } from "util/objectUtils";
3434
import { CompTree, getAllCompItems, IContainer } from "../containerBase";
3535
import { SimpleContainerComp, toSimpleContainerData } from "../containerBase/simpleContainerComp";
@@ -40,6 +40,7 @@ import { listPropertyView } from "./listViewPropertyView";
4040
import { getData } from "./listViewUtils";
4141
import { withMethodExposing } from "comps/generators/withMethodExposing";
4242
import { SliderControl } from "@lowcoder-ee/comps/controls/sliderControl";
43+
import { eventHandlerControl, sortChangeEvent } from "@lowcoder-ee/comps/controls/eventHandlerControl";
4344

4445
const childrenMap = {
4546
noOfRows: withIsLoadingMethod(NumberOrJSONObjectArrayControl), // FIXME: migrate "noOfRows" to "data"
@@ -62,6 +63,7 @@ const childrenMap = {
6263
horizontal: withDefault(BoolControl, false),
6364
minHorizontalWidth: withDefault(RadiusControl, '100px'),
6465
enableSorting: withDefault(BoolControl, false),
66+
onEvent: eventHandlerControl([sortChangeEvent] as const),
6567
};
6668

6769
const ListViewTmpComp = new UICompBuilder(childrenMap, () => <></>)
@@ -183,6 +185,15 @@ ListViewPropertyComp = withExposingConfigs(ListViewPropertyComp, [
183185
return data;
184186
},
185187
}),
188+
depsConfig({
189+
name: "sortedData",
190+
desc: trans("listView.dataDesc"),
191+
depKeys: ["listData"],
192+
func: (input) => {
193+
const { data } = getData(input.listData as JSONObject[]);
194+
return data;
195+
},
196+
}),
186197
new CompDepsConfig(
187198
"pageNo",
188199
(comp) => ({index: comp.children.pagination.children.pageNo.exposingNode() }),

client/packages/lowcoder/src/comps/comps/listViewComp/listViewPropertyView.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export function listPropertyView(compType: ListCompType) {
5555

5656
{(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && (
5757
<Section name={sectionNames.interaction}>
58+
{children.onEvent.getPropertyView()}
5859
{hiddenPropertyView(children)}
5960
{showDataLoadingIndicatorsPropertyView(children)}
6061
{children.enableSorting.propertyView({

client/packages/lowcoder/src/comps/controls/eventHandlerControl.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,11 @@ export const resetEvent: EventConfigType = {
489489
value: "reset",
490490
description: trans("event.resetDesc"),
491491
};
492-
492+
export const sortChangeEvent: EventConfigType = {
493+
label: trans("event.sortChange"),
494+
value: "sortChange",
495+
description: trans("event.sortChangeDesc"),
496+
};
493497

494498
// Meeting Events
495499

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

+9
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,8 @@ export const en = {
463463
"resetDesc": "Triggers on Reset timer",
464464
"refresh": "Refresh",
465465
"refreshDesc": "Triggers on Refresh",
466+
"sortChange": "Sort Change",
467+
"sortChangeDesc": "Triggers on Sorting Changes",
466468
},
467469

468470

@@ -2484,6 +2486,12 @@ export const en = {
24842486
"inviteUserLabel": "Invitation Link:",
24852487
"inviteCopyLink": "Copy Link",
24862488
"inviteText": "{userName} Invites You to Join the Workspace \"{organization}\", Click on the Link to Join: {inviteLink}",
2489+
"inviteByEmailHelp": "You can enter one or more email addresses to send invitation links",
2490+
"inviteByEmailLabel": "Enter emails:",
2491+
"inviteByEmailButton": "Send Invitations",
2492+
"inviteByEmailSuccess": "Invitations sent successfully!",
2493+
"inviteByEmailError": "Something went wrong while sending invitations. Please try again.",
2494+
"noValidEmails": "No valid emails found",
24872495
"groupName": "Group Name",
24882496
"createTime": "Create Time",
24892497
"manageBtn": "Manage",
@@ -3079,6 +3087,7 @@ export const en = {
30793087
"memberOfOrgs": "Workspaces Membership",
30803088
"apiKeys": "API Keys",
30813089
"createApiKey": "Create API Key",
3090+
"apiKeyInfo": "Make sure to copy your new API key now. You won't be able to see it again.",
30823091
"apiKeyName": "Name",
30833092
"apiKeyDescription": "Description",
30843093
"apiKeyCopy": "Click the Api Key to get the value in your clipboard",

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

+8-3
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

+24-14
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/ApplicationV2/index.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,15 @@ export default function ApplicationHome() {
104104
if (user.currentOrgId) {
105105
dispatch(fetchDeploymentIdAction());
106106
}
107-
dispatch(fetchHomeData({}));
108107
}, [user.currentOrgId]);
109108

109+
useEffect(() => {
110+
// tricky check, will be called for anonymous user to redirect to login page
111+
if (user.isAnonymous) {
112+
dispatch(fetchHomeData({}));
113+
}
114+
}, [user.isAnonymous])
115+
110116
useEffect(() => {
111117
if(Boolean(deploymentId)) {
112118
dispatch(fetchSubscriptionsAction())

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

+56-3
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/common/inviteLanding.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function InviteLanding(props: InviteLandingProp) {
4545
orgId = inviteInfo.invitedOrganizationId;
4646
const inviteState = inviteInfo ? { ...inviteInfo, invitationId } : { invitationId };
4747
history.push({
48-
pathname: AUTH_LOGIN_URL,
48+
pathname: `/org/${orgId}/auth/login`,
4949
state: {
5050
inviteInfo: inviteState,
5151
},

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ export default function FormLogin(props: FormLoginProps) {
3131
const [password, setPassword] = useState("");
3232
const { fetchUserAfterAuthSuccess } = useContext(AuthContext);
3333

34+
const afterLoginSuccess = () => {
35+
if (props.organizationId) {
36+
localStorage.setItem("lowcoder_login_orgId", props.organizationId);
37+
}
38+
fetchUserAfterAuthSuccess?.();
39+
}
40+
3441
const { onSubmit, loading } = useAuthSubmit(
3542
() =>
3643
UserApi.formLogin({
@@ -42,7 +49,7 @@ export default function FormLogin(props: FormLoginProps) {
4249
}),
4350
false,
4451
null,
45-
fetchUserAfterAuthSuccess,
52+
afterLoginSuccess,
4653
);
4754

4855
return (

0 commit comments

Comments
 (0)