From cebce0cc823217fc7f10dcc3ce1c35b31557e2d4 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Mon, 13 Nov 2023 18:03:28 +0500 Subject: [PATCH 01/24] fix: update lowcoder-sdk version --- client/packages/lowcoder-sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder-sdk/package.json b/client/packages/lowcoder-sdk/package.json index 1c5f39d03..051c06a3c 100644 --- a/client/packages/lowcoder-sdk/package.json +++ b/client/packages/lowcoder-sdk/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-sdk", - "version": "2.1.7", + "version": "2.1.8", "type": "module", "files": [ "src", From 6b48abd199699cfb9d2e16b41d3ca2625bd61249 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Tue, 14 Nov 2023 00:47:44 +0500 Subject: [PATCH 02/24] Fix redirect after auth login (#499) * fix: update lowcoder-sdk version * fix: redirect after auth --------- Co-authored-by: Falk Wolsky --- client/packages/lowcoder-sdk/package.json | 2 +- client/packages/lowcoder/src/pages/userAuth/authUtils.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder-sdk/package.json b/client/packages/lowcoder-sdk/package.json index 1c5f39d03..051c06a3c 100644 --- a/client/packages/lowcoder-sdk/package.json +++ b/client/packages/lowcoder-sdk/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-sdk", - "version": "2.1.7", + "version": "2.1.8", "type": "module", "files": [ "src", diff --git a/client/packages/lowcoder/src/pages/userAuth/authUtils.ts b/client/packages/lowcoder/src/pages/userAuth/authUtils.ts index 9a05802df..36363b778 100644 --- a/client/packages/lowcoder/src/pages/userAuth/authUtils.ts +++ b/client/packages/lowcoder/src/pages/userAuth/authUtils.ts @@ -78,6 +78,7 @@ export function authRespValidate( onAuthSuccess?: () => void ) { let replaceUrl = redirectUrl || BASE_URL; + const baseUrl = `${window.location.protocol}//${window.location.host}`; if (infoCompleteCheck) { // need complete info replaceUrl = redirectUrl @@ -86,7 +87,7 @@ export function authRespValidate( } if (doValidResponse(resp)) { onAuthSuccess?.(); - history.replace(replaceUrl); + history.replace(replaceUrl.replace(baseUrl, '')); } else if ( resp.data.code === SERVER_ERROR_CODES.EXCEED_MAX_USER_ORG_COUNT || resp.data.code === SERVER_ERROR_CODES.ALREADY_IN_ORGANIZATION From e907f17678fe43329cbcac6a09643c8a8c123664 Mon Sep 17 00:00:00 2001 From: freddysundowner Date: Thu, 16 Nov 2023 11:21:42 +0300 Subject: [PATCH 03/24] added data field to add external token server --- .../videoMeetingControllerComp.tsx | 46 +++++++++---------- .../packages/lowcoder/src/i18n/locales/en.ts | 6 ++- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx index 427feabb3..ae67cebaf 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx @@ -111,18 +111,21 @@ let screenShareStream: ILocalVideoTrack; let userId: UID | null | undefined; let rtmChannelResponse: RtmChannel; let rtmClient: RtmClient; -const agoraTokenUrl = `https://sandbox.wiggolive.com/token/rtc`; const generateToken = async ( appId: any, certificate: any, - channelName: any + channelName: any, + serverurl: any ) => { - let response = await axios.post(agoraTokenUrl, { - appId, - certificate, - channelName, - }); + let response = await axios.post( + serverurl ?? "https://sandbox.wiggolive.com/token/rtc", + { + appId, + certificate, + channelName, + } + ); return response.data; }; @@ -179,17 +182,16 @@ const leaveChannel = async () => { await rtmChannelResponse.leave(); }; -const hostChanged = (users: any) => {}; - const publishVideo = async ( appId: string, channel: any, height: any, - certifiCateKey: string + certifiCateKey: string, + serverurl: string ) => { let token = null; if (certifiCateKey) { - token = await generateToken(appId, certifiCateKey, channel); + token = await generateToken(appId, certifiCateKey, channel, serverurl); } await turnOnCamera(true); await client.join(appId, channel, token, userId); @@ -234,6 +236,7 @@ export const meetingControllerChildren = { endCall: withDefault(BooleanStateControl, "false"), sharing: withDefault(BooleanStateControl, "false"), appId: withDefault(StringControl, trans("meeting.appid")), + tokenServerUrl: withDefault(StringControl, trans("meeting.serverurl")), participants: stateComp([]), usersScreenShared: stateComp([]), localUser: jsonObjectExposingStateControl(""), @@ -276,27 +279,23 @@ let MTComp = (function () { useEffect(() => { if (userJoined) { + let prevUsers: any[] = props.participants as []; let userData = { user: userJoined.uid, - host: false, audiostatus: userJoined.hasAudio, streamingVideo: true, }; setUserIds((userIds: any) => [...userIds, userData]); - if (userIds.length == 0) { - userData.host = true; - } else { - userData.host = false; - } dispatch( changeChildAction( "participants", - removeDuplicates(getData([...userIds, userData]).data, "user"), + removeDuplicates(getData([...prevUsers, userData]).data, "user"), false ) ); } }, [userJoined]); + function removeDuplicates(arr: any, prop: any) { const uniqueObjects = []; const seenValues = new Set(); @@ -320,7 +319,6 @@ let MTComp = (function () { let hostExists = newUsers.filter((f: any) => f.host === true); if (hostExists.length == 0 && newUsers.length > 0) { newUsers[0].host = true; - hostChanged(newUsers); } setUserIds(newUsers); dispatch( @@ -432,8 +430,6 @@ let MTComp = (function () { client.on( "user-unpublished", (user: IAgoraRTCRemoteUser, mediaType: "video" | "audio") => { - console.log("user-unpublished"); - setLocalUserVideo(user); } ); @@ -511,10 +507,12 @@ let MTComp = (function () { {children.certifiCateKey.propertyView({ label: trans("meeting.certifiCateKey"), })} - {children.meetingName.propertyView({ label: trans("meeting.meetingName"), })} + {children.tokenServerUrl.propertyView({ + label: trans("meeting.serverurl"), + })} {children.placement.propertyView({ label: trans("drawer.placement"), @@ -636,6 +634,7 @@ MTComp = withMethodExposing(MTComp, [ params: [], }, execute: async (comp, values) => { + if (comp.children.meetingActive.getView().value) return; userId = Math.floor(100000 + Math.random() * 900000); comp.children.localUser.change({ user: userId + "", @@ -663,7 +662,8 @@ MTComp = withMethodExposing(MTComp, [ ? "_meetingId" : comp.children.meetingName.getView().value, comp.children, - comp.children.certifiCateKey.getView().value + comp.children.certifiCateKey.getView().value, + comp.children.tokenServerUrl.getView() ); comp.children.meetingActive.change(true); }, diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 7f20f9c9e..717764bb4 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -2,7 +2,8 @@ import table from "./componentDocExtra/table.md?url"; export const en = { productName: "Lowcoder", - productDesc: "Create software applications for your Company and your Customers with minimal coding experience. Lowcoder is the best Retool, Appsmith or Tooljet Alternative.", + productDesc: + "Create software applications for your Company and your Customers with minimal coding experience. Lowcoder is the best Retool, Appsmith or Tooljet Alternative.", notSupportedBrowser: "Your current browser may have compatibility issues. For a better user experience, it is recommended to use the latest version of the Chrome browser.", create: "Create", @@ -1458,6 +1459,7 @@ export const en = { participants: "Participants", shareScreen: "Share Screen", appid: "Application Id", + serverurl: "Token Server url", meetingName: "Meeting Name", videoCompText: "No video Text", profileImageUrl: "Profile Image Url", @@ -2094,7 +2096,7 @@ export const en = { resetSuccessDesc: "Password reset succeeded. The new password is: {password}", copyPassword: "Copy password", - poweredByLowcoder: "Powered by Lowcoder.cloud" + poweredByLowcoder: "Powered by Lowcoder.cloud", }, preLoad: { jsLibraryHelpText: From a5b3308f9edb4d61f6f6ef9af049e4dad8f9d158 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Sun, 19 Nov 2023 12:15:01 +0100 Subject: [PATCH 04/24] Agora Token Handling and small Fixes --- .../src/components/Section.tsx | 1 + .../src/i18n/design/locales/en.ts | 1 + .../comps/comps/meetingComp/controlButton.tsx | 16 +- .../videoMeetingControllerComp.tsx | 146 ++++++++++-------- .../meetingComp/videoMeetingStreamComp.tsx | 5 +- .../packages/lowcoder/src/i18n/locales/en.ts | 25 +-- 6 files changed, 115 insertions(+), 79 deletions(-) diff --git a/client/packages/lowcoder-design/src/components/Section.tsx b/client/packages/lowcoder-design/src/components/Section.tsx index feae5402a..a0c18134f 100644 --- a/client/packages/lowcoder-design/src/components/Section.tsx +++ b/client/packages/lowcoder-design/src/components/Section.tsx @@ -142,4 +142,5 @@ export const sectionNames = { validation: trans("prop.validation"), layout: trans("prop.layout"), style: trans("prop.style"), + meetings : trans("prop.meetings"), }; diff --git a/client/packages/lowcoder-design/src/i18n/design/locales/en.ts b/client/packages/lowcoder-design/src/i18n/design/locales/en.ts index 0082f6f98..59e045b55 100644 --- a/client/packages/lowcoder-design/src/i18n/design/locales/en.ts +++ b/client/packages/lowcoder-design/src/i18n/design/locales/en.ts @@ -23,6 +23,7 @@ export const en = { validation: "Validation", layout: "Layout", style: "Style", + meetings : "Meeting Settings", }, passwordInput: { label: "Password:", diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx index d3ad399a5..b6034a6db 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx @@ -179,6 +179,7 @@ let ButtonTmpComp = (function () { iconSize: withDefault(StringControl, "20px"), type: dropdownControl(typeOptions, ""), autoHeight: withDefault(AutoHeightControl, "fixed"), + aspectRatio: withDefault(StringControl, "1 / 1"), onEvent: ButtonEventHandlerControl, disabled: BoolCodeControl, loading: BoolCodeControl, @@ -244,8 +245,16 @@ let ButtonTmpComp = (function () { loading={props.loading} style={ props.autoHeight - ? { width: "100%", height: "100%" } - : undefined + ? { + width: "100%", + height: "100%", + aspectRatio: props.aspectRatio, + borderRadius: props.style.radius, + } + : { + aspectRatio: props.aspectRatio, + borderRadius: props.style.radius, + } } disabled={ props.disabled || @@ -304,6 +313,9 @@ let ButtonTmpComp = (function () {
{children.style.getPropertyView()} + {children.aspectRatio.propertyView({ + label: "Video Aspect Ratio", + })}
)) diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx index ae67cebaf..49a5f2cbb 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx @@ -41,7 +41,10 @@ import { useUserViewMode } from "util/hooks"; import { isNumeric } from "util/stringUtils"; import { NameConfig, withExposingConfigs } from "../../generators/withExposing"; -import axios from "axios"; +import { v4 as uuidv4 } from 'uuid'; + +// import axios from "axios"; + import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack, @@ -51,7 +54,7 @@ import AgoraRTC, { ILocalVideoTrack, } from "agora-rtc-sdk-ng"; -import { JSONValue } from "@lowcoder-ee/index.sdk"; +import { JSONValue, NumberControl } from "@lowcoder-ee/index.sdk"; import { getData } from "../listViewComp/listViewUtils"; import AgoraRTM, { RtmChannel, RtmClient } from "agora-rtm-sdk"; @@ -103,7 +106,16 @@ export const client: IAgoraRTCClient = AgoraRTC.createClient({ mode: "rtc", codec: "vp8", }); -AgoraRTC.setLogLevel(3); + +AgoraRTC.setLogLevel(4); + +/* +0: DEBUG. Output all API logs. +1: INFO. Output logs of the INFO, WARNING and ERROR level. +2: WARNING. Output logs of the WARNING and ERROR level. +3: ERROR. Output logs of the ERROR level. +4: NONE. Do not output any log. +*/ let audioTrack: IMicrophoneAudioTrack; let videoTrack: ICameraVideoTrack; @@ -112,23 +124,6 @@ let userId: UID | null | undefined; let rtmChannelResponse: RtmChannel; let rtmClient: RtmClient; -const generateToken = async ( - appId: any, - certificate: any, - channelName: any, - serverurl: any -) => { - let response = await axios.post( - serverurl ?? "https://sandbox.wiggolive.com/token/rtc", - { - appId, - certificate, - channelName, - } - ); - return response.data; -}; - const turnOnCamera = async (flag?: boolean) => { if (videoTrack) { return videoTrack.setEnabled(flag!); @@ -150,7 +145,7 @@ const turnOnMicrophone = async (flag?: boolean) => { }; const shareScreen = async (sharing: boolean) => { try { - if (sharing == false) { + if (sharing === false) { await client.unpublish(screenShareStream); await client.publish(videoTrack); videoTrack.play(userId + ""); @@ -184,20 +179,16 @@ const leaveChannel = async () => { const publishVideo = async ( appId: string, - channel: any, - height: any, - certifiCateKey: string, - serverurl: string + channel: string, + rtmToken: string, + rtcToken: string ) => { - let token = null; - if (certifiCateKey) { - token = await generateToken(appId, certifiCateKey, channel, serverurl); - } - await turnOnCamera(true); - await client.join(appId, channel, token, userId); - await client.publish(videoTrack); - - await rtmInit(appId, userId, channel); + // initializing the Agora Meeting Client + await turnOnCamera(true); + await client.join(appId, channel, rtcToken, userId); + await client.publish(videoTrack); + // initializing the Agora RTM Client + await rtmInit(appId, userId, rtmToken, channel); }; const sendMessageRtm = (message: any) => { @@ -208,10 +199,11 @@ const sendPeerMessageRtm = (message: any, toId: string) => { rtmClient.sendMessageToPeer({ text: JSON.stringify(message) }, toId); }; -const rtmInit = async (appId: any, uid: any, channel: any) => { +const rtmInit = async (appId: any, uid: any, token: any, channel: any) => { rtmClient = AgoraRTM.createInstance(appId); let options = { uid: String(uid), + token: token ? token : null, }; await rtmClient.login(options); @@ -236,12 +228,13 @@ export const meetingControllerChildren = { endCall: withDefault(BooleanStateControl, "false"), sharing: withDefault(BooleanStateControl, "false"), appId: withDefault(StringControl, trans("meeting.appid")), - tokenServerUrl: withDefault(StringControl, trans("meeting.serverurl")), participants: stateComp([]), usersScreenShared: stateComp([]), localUser: jsonObjectExposingStateControl(""), - meetingName: stringStateControl("meetingName"), - certifiCateKey: stringStateControl(""), + localUserID : withDefault(stringStateControl(trans("meeting.localUserID")), uuidv4() + ""), + meetingName: withDefault(stringStateControl(trans("meeting.meetingName")), uuidv4() + ""), + rtmToken: stringStateControl(trans("meeting.rtmToken")), + rtcToken: stringStateControl(trans("meeting.rtcToken")), messages: stateComp([]), }; let MTComp = (function () { @@ -272,8 +265,7 @@ let MTComp = (function () { }); const [rtmMessages, setRtmMessages] = useState([]); const [localUserSpeaking, setLocalUserSpeaking] = useState(false); - const [localUserVideo, setLocalUserVideo] = - useState(); + const [localUserVideo, setLocalUserVideo] = useState(); const [userJoined, setUserJoined] = useState(); const [userLeft, setUserLeft] = useState(); @@ -503,17 +495,6 @@ let MTComp = (function () { .setPropertyViewFn((children) => ( <>
- {children.appId.propertyView({ label: trans("meeting.appid") })} - {children.certifiCateKey.propertyView({ - label: trans("meeting.certifiCateKey"), - })} - {children.meetingName.propertyView({ - label: trans("meeting.meetingName"), - })} - {children.tokenServerUrl.propertyView({ - label: trans("meeting.serverurl"), - })} - {children.placement.propertyView({ label: trans("drawer.placement"), radioButton: true, @@ -539,6 +520,23 @@ let MTComp = (function () { label: trans("prop.showMask"), })}
+
+ {children.appId.propertyView({ + label: trans("meeting.appid") + })} + {children.meetingName.propertyView({ + label: trans("meeting.meetingName"), + })} + {children.localUserID.propertyView({ + label: trans("meeting.localUserID"), + })} + {children.rtmToken.propertyView({ + label: trans("meeting.rtmToken"), + })} + {children.rtcToken.propertyView({ + label: trans("meeting.rtcToken"), + })} +
{children.onEvent.getPropertyView()}
@@ -635,7 +633,7 @@ MTComp = withMethodExposing(MTComp, [ }, execute: async (comp, values) => { if (comp.children.meetingActive.getView().value) return; - userId = Math.floor(100000 + Math.random() * 900000); + userId = comp.children.localUserID.getView().value === "" ? uuidv4() : comp.children.localUserID.getView().value; comp.children.localUser.change({ user: userId + "", audiostatus: false, @@ -658,12 +656,9 @@ MTComp = withMethodExposing(MTComp, [ comp.children.videoControl.change(true); await publishVideo( comp.children.appId.getView(), - comp.children.meetingName.getView().value == "" - ? "_meetingId" - : comp.children.meetingName.getView().value, - comp.children, - comp.children.certifiCateKey.getView().value, - comp.children.tokenServerUrl.getView() + comp.children.meetingName.getView().value === "" ? uuidv4() : comp.children.meetingName.getView().value, + comp.children.rtmToken.getView().value, + comp.children.rtcToken.getView().value ); comp.children.meetingActive.change(true); }, @@ -677,9 +672,9 @@ MTComp = withMethodExposing(MTComp, [ execute: async (comp, values) => { if (!comp.children.meetingActive.getView().value) return; let otherData = - values != undefined && values[1] !== undefined ? values[1] : ""; + values !== undefined && values[1] !== undefined ? values[1] : ""; let toUsers: any = - values != undefined && values[0] !== undefined ? values[0] : ""; + values !== undefined && values[0] !== undefined ? values[0] : ""; let message: any = { time: Date.now(), @@ -711,7 +706,7 @@ MTComp = withMethodExposing(MTComp, [ { method: { name: "setUserName", - description: trans("meeting.meetingName"), + description: trans("meeting.userName"), params: [], }, execute: async (comp, values) => { @@ -720,6 +715,28 @@ MTComp = withMethodExposing(MTComp, [ comp.children.localUser.change({ ...userLocal, userName: userName }); }, }, + { + method: { + name: "setRTCToken", + description: trans("meeting.rtcToken"), + params: [], + }, + execute: async (comp, values) => { + let rtcToken: any = values[0]; + comp.children.rtcToken.change(rtcToken); + }, + }, + { + method: { + name: "setRTMToken", + description: trans("meeting.rtmToken"), + params: [], + }, + execute: async (comp, values) => { + let rtmToken: any = values[0]; + comp.children.rtmToken.change(rtmToken); + }, + }, { method: { name: "endMeeting", @@ -758,7 +775,10 @@ export const VideoMeetingControllerComp = withExposingConfigs(MTComp, [ new NameConfig("appId", trans("meeting.appid")), new NameConfig("localUser", trans("meeting.host")), new NameConfig("participants", trans("meeting.participants")), - new NameConfig("meetingActive", trans("meeting.meetingName")), + new NameConfig("meetingActive", trans("meeting.meetingActive")), new NameConfig("meetingName", trans("meeting.meetingName")), - new NameConfig("messages", trans("meeting.meetingName")), + new NameConfig("localUserID", trans("meeting.localUserID")), + new NameConfig("messages", trans("meeting.messages")), + new NameConfig("rtmToken", trans("meeting.rtmToken")), + new NameConfig("rtcToken", trans("meeting.rtcToken")), ]); diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingStreamComp.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingStreamComp.tsx index 2218ba0c3..5fbc372b7 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingStreamComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingStreamComp.tsx @@ -151,10 +151,7 @@ export const meetingStreamChildren = { style: ButtonStyleControl, viewRef: RefControl, userId: stringExposingStateControl(""), - profileImageUrl: withDefault( - StringStateControl, - "https://via.placeholder.com/120" - ), + profileImageUrl: withDefault(StringStateControl, "https://api.dicebear.com/7.x/fun-emoji/svg?seed=Peanut&radius=50&backgroundColor=transparent&randomizeIds=true&eyes=wink,sleepClose"), noVideoText: stringExposingStateControl("No Video"), }; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 717764bb4..40138fb17 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1442,6 +1442,7 @@ export const en = { height: "Drawer height", }, meeting: { + logLevel: "Agora SDK Log Level", placement: "Meeting placement", meeting: "Meeting Settings", cameraView: "Camera View", @@ -1455,18 +1456,21 @@ export const en = { videoOn: "Video On", size: "Size", top: "Top", - host: "Host", - participants: "Participants", - shareScreen: "Share Screen", - appid: "Application Id", - serverurl: "Token Server url", + host: "Host of the Meetingroom", + participants: "Participants of the Meetingroom", + shareScreen: "Local Screenshare", + appid: "Agora Application Id", meetingName: "Meeting Name", + localUserID: "Host User Id", + userName: "Host User Name", + rtmToken : "Agora RTM Token", + rtcToken : "Agora RTC Token", videoCompText: "No video Text", profileImageUrl: "Profile Image Url", right: "Right", bottom: "Bottom", - videoId: "Video Id", - audioStatus: "audio status", + videoId: "Video Stream Id", + audioStatus: "Audio status", left: "Left", widthTooltip: "Number or percentage, e.g. 520, 60%", heightTooltip: "Number, e.g. 378", @@ -1476,11 +1480,10 @@ export const en = { height: "Drawer height", actionBtnDesc: "Action Button", broadCast: "BroadCast Messages", - certifiCateKey: "certifiCate Key", - title: "Meeting title", + title: "Meeting Title", meetingCompName: "Meeting Controller", videoCompName: "Video Stream", - videoSharingCompName: "Video Sharing", + videoSharingCompName: "Screen Sharing", meetingControlCompName: "Controls Buttons", meetingCompDesc: "Meeting component", meetingCompControls: "Meeting control", @@ -1488,6 +1491,8 @@ export const en = { iconSize: "Icon Size", userId: "userId", roomId: "roomId", + meetingActive : "Ongoing Meeting", + messages : "Broadcasted Messages", }, settings: { title: "Settings", From 1e4f02a56d5f819f944f7312627462414af7fdbd Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Mon, 20 Nov 2023 08:58:27 +0100 Subject: [PATCH 05/24] Updated Translations --- client/packages/lowcoder/src/i18n/locales/en.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 40138fb17..c061179ef 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -318,6 +318,7 @@ export const en = { validate: "Validation message", border: "Border", borderRadius: "Border radius", + borderwidth: "Border width", background: "Background", headerBackground: "Header background", footerBackground: "Footer background", From e03639fc341f6a73ac0cd5d3310fcf13a8042a81 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Mon, 20 Nov 2023 09:08:40 +0100 Subject: [PATCH 06/24] Updated Section Control --- client/packages/lowcoder-design/src/components/Section.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder-design/src/components/Section.tsx b/client/packages/lowcoder-design/src/components/Section.tsx index a0c18134f..da58f9588 100644 --- a/client/packages/lowcoder-design/src/components/Section.tsx +++ b/client/packages/lowcoder-design/src/components/Section.tsx @@ -142,5 +142,5 @@ export const sectionNames = { validation: trans("prop.validation"), layout: trans("prop.layout"), style: trans("prop.style"), - meetings : trans("prop.meetings"), + meetings : trans("prop.meetings"), // added by Falk Wolsky }; From 2e13bff11546ea3a6a38dc36bef1fd39409058f5 Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Mon, 20 Nov 2023 11:10:16 +0100 Subject: [PATCH 07/24] fix: change oauth url format to allow whole base url --- .../lowcoder/sdk/auth/Oauth2KeycloakAuthConfig.java | 10 +++++----- .../org/lowcoder/sdk/auth/Oauth2OryAuthConfig.java | 10 +++++----- .../sdk/auth/constants/Oauth2Constants.java | 6 +++--- .../request/oauth2/Oauth2DefaultSource.java | 13 ++++++------- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2KeycloakAuthConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2KeycloakAuthConfig.java index a6395f972..33408daf8 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2KeycloakAuthConfig.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2KeycloakAuthConfig.java @@ -1,6 +1,6 @@ package org.lowcoder.sdk.auth; -import static org.lowcoder.sdk.auth.constants.Oauth2Constants.INSTANCE_ID_PLACEHOLDER; +import static org.lowcoder.sdk.auth.constants.Oauth2Constants.BASE_URL_PLACEHOLDER; import static org.lowcoder.sdk.auth.constants.Oauth2Constants.REALM_PLACEHOLDER; import com.fasterxml.jackson.annotation.JsonCreator; @@ -15,7 +15,7 @@ @Getter public class Oauth2KeycloakAuthConfig extends Oauth2SimpleAuthConfig { - protected String instanceId; + protected String baseUrl; protected String realm; @JsonCreator @@ -27,12 +27,12 @@ public Oauth2KeycloakAuthConfig( @JsonProperty("sourceName") String sourceName, @JsonProperty("clientId") String clientId, @JsonProperty("clientSecret") String clientSecret, - @JsonProperty("instanceId") String instanceId, + @JsonProperty("baseUrl") String baseUrl, @JsonProperty("realm") String realm, @JsonProperty("authType") String authType) { super(id, enable, enableRegister, source, sourceName, clientId, clientSecret, authType); - this.instanceId = instanceId; + this.baseUrl = baseUrl; this.realm = realm; } @@ -42,7 +42,7 @@ public Oauth2KeycloakAuthConfig( public String replaceAuthUrlClientIdPlaceholder(String url) { return super.replaceAuthUrlClientIdPlaceholder(url) - .replace(INSTANCE_ID_PLACEHOLDER, instanceId) + .replace(BASE_URL_PLACEHOLDER, baseUrl) .replace(REALM_PLACEHOLDER, realm); } diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2OryAuthConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2OryAuthConfig.java index 345e05b96..8c5a13209 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2OryAuthConfig.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/Oauth2OryAuthConfig.java @@ -1,6 +1,6 @@ package org.lowcoder.sdk.auth; -import static org.lowcoder.sdk.auth.constants.Oauth2Constants.INSTANCE_ID_PLACEHOLDER; +import static org.lowcoder.sdk.auth.constants.Oauth2Constants.BASE_URL_PLACEHOLDER; import javax.annotation.Nullable; @@ -14,7 +14,7 @@ @Getter public class Oauth2OryAuthConfig extends Oauth2SimpleAuthConfig { - protected String instanceId; + protected String baseUrl; @JsonCreator public Oauth2OryAuthConfig( @@ -25,14 +25,14 @@ public Oauth2OryAuthConfig( String sourceName, String clientId, String clientSecret, - String instanceId, + String baseUrl, String authType) { super(id, enable, enableRegister, source, sourceName, clientId, clientSecret, authType); - this.instanceId = instanceId; + this.baseUrl = baseUrl; } @Override public String replaceAuthUrlClientIdPlaceholder(String url) { - return super.replaceAuthUrlClientIdPlaceholder(url).replace(INSTANCE_ID_PLACEHOLDER, instanceId); + return super.replaceAuthUrlClientIdPlaceholder(url).replace(BASE_URL_PLACEHOLDER, baseUrl); } } diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/constants/Oauth2Constants.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/constants/Oauth2Constants.java index 6313af520..3b3bcc917 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/constants/Oauth2Constants.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/auth/constants/Oauth2Constants.java @@ -8,7 +8,7 @@ public class Oauth2Constants { public static final String STATE_PLACEHOLDER = "$STATE"; public static final String REALM_PLACEHOLDER = "$REALM"; - public static final String INSTANCE_ID_PLACEHOLDER = "$INSTANCE_ID"; + public static final String BASE_URL_PLACEHOLDER = "$BASE_URL"; // authorize url public static final String GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize" @@ -27,14 +27,14 @@ public class Oauth2Constants { + "&scope=openid email profile" + "&prompt=select_account"; - public static final String ORY_AUTHORIZE_URL = "https://" + INSTANCE_ID_PLACEHOLDER + "/oauth2/auth" + public static final String ORY_AUTHORIZE_URL = BASE_URL_PLACEHOLDER + "/oauth2/auth" + "?response_type=code" + "&client_id=" + CLIENT_ID_PLACEHOLDER + "&redirect_uri=" + REDIRECT_URL_PLACEHOLDER + "&state=" + STATE_PLACEHOLDER + "&scope=openid email profile offline_access"; - public static final String KEYCLOAK_AUTHORIZE_URL = "https://" + INSTANCE_ID_PLACEHOLDER + "/realms/" + REALM_PLACEHOLDER + "/protocol/openid-connect/auth" + public static final String KEYCLOAK_AUTHORIZE_URL = BASE_URL_PLACEHOLDER + "/realms/" + REALM_PLACEHOLDER + "/protocol/openid-connect/auth" + "?response_type=code" + "&client_id=" + CLIENT_ID_PLACEHOLDER + "&redirect_uri=" + REDIRECT_URL_PLACEHOLDER diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2DefaultSource.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2DefaultSource.java index 70a7cc776..8a2d1a281 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2DefaultSource.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/request/oauth2/Oauth2DefaultSource.java @@ -1,7 +1,6 @@ package org.lowcoder.api.authentication.request.oauth2; import org.lowcoder.sdk.auth.constants.Oauth2Constants; -import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; public enum Oauth2DefaultSource implements Oauth2Source { @@ -43,17 +42,17 @@ public String refresh() { ORY { @Override public String accessToken() { - return "https://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/oauth2/token"; + return Oauth2Constants.BASE_URL_PLACEHOLDER + "/oauth2/token"; } @Override public String userInfo() { - return "https://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/userinfo"; + return Oauth2Constants.BASE_URL_PLACEHOLDER + "/userinfo"; } @Override public String refresh() { - return "https://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/oauth2/token"; + return Oauth2Constants.BASE_URL_PLACEHOLDER + "/oauth2/token"; } }, @@ -62,17 +61,17 @@ public String refresh() { @Override public String accessToken() { - return "http://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/token"; + return Oauth2Constants.BASE_URL_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/token"; } @Override public String userInfo() { - return "http://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/userinfo"; + return Oauth2Constants.BASE_URL_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/userinfo"; } @Override public String refresh() { - return "http://" + Oauth2Constants.INSTANCE_ID_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/token"; + return Oauth2Constants.BASE_URL_PLACEHOLDER + "/realms/" + Oauth2Constants.REALM_PLACEHOLDER + "/protocol/openid-connect/token"; } } From 538065511d7798a4747a1132347aafc63e165b21 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Mon, 20 Nov 2023 13:31:18 +0100 Subject: [PATCH 08/24] Broadcast Function including Array Length --- .../videoMeetingControllerComp.tsx | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx index 49a5f2cbb..fe4bcdb16 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx @@ -378,16 +378,29 @@ let MTComp = (function () { useEffect(() => { if (rtmChannelResponse) { rtmClient.on("MessageFromPeer", function (message, peerId) { - setRtmMessages(message.text); + setRtmMessages((prevMessages: any[]) => { + // Check if the messages array exceeds the maximum limit + if (prevMessages.length >= 500) { + prevMessages.pop(); // Remove the oldest message + } + return [...prevMessages, {"peermessage" : JSON.parse(message.text + ""), "from" : peerId}]; + }); }); + rtmChannelResponse.on("ChannelMessage", function (message, memberId) { - setRtmMessages(message.text); - dispatch( - changeChildAction("messages", getData(rtmMessages).data, false) - ); + setRtmMessages((prevMessages: any[]) => { + // Check if the messages array exceeds the maximum limit + if (prevMessages.length >= 500) { + prevMessages.pop(); // Remove the oldest message + } + return [...prevMessages, {"channelmessage" : JSON.parse(message.text + ""), "from" : memberId}]; + }); + + dispatch(changeChildAction("messages", getData(rtmMessages).data, false)); }); } }, [rtmChannelResponse]); + useEffect(() => { if (client) { @@ -399,11 +412,11 @@ let MTComp = (function () { setUserLeft(user); }); client.on("volume-indicator", (volumeInfos: any) => { - if (volumeInfos.length == 0) return; + if (volumeInfos.length === 0) return; volumeInfos.map((volumeInfo: any) => { const speaking = volumeInfo.level >= 30; if ( - volumeInfo.uid == userId && + volumeInfo.uid === userId && props.localUser.value.speaking != speaking ) { setLocalUserSpeaking(speaking); @@ -671,21 +684,20 @@ MTComp = withMethodExposing(MTComp, [ }, execute: async (comp, values) => { if (!comp.children.meetingActive.getView().value) return; - let otherData = - values !== undefined && values[1] !== undefined ? values[1] : ""; - let toUsers: any = + let messagedata = values !== undefined && values[0] !== undefined ? values[0] : ""; + let toUsers: any = + values !== undefined && values[1] !== undefined ? values[1] : ""; let message: any = { time: Date.now(), - from: comp.children.localUser.getView().value, + message: messagedata, }; - message["data"] = otherData; if (toUsers.length > 0 && toUsers[0] !== undefined) { - let peers = toUsers?.map((u: any) => u.user); - peers.forEach((p: any) => { - sendPeerMessageRtm(message, String(p)); + toUsers.forEach((peer: any) => { + message.to = peer; + sendPeerMessageRtm(message, String(peer)); }); } else { sendMessageRtm(message); From aa9a3e14bc98091d8b8da36782f5faaf2ebf006e Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Mon, 20 Nov 2023 15:50:51 +0100 Subject: [PATCH 09/24] new: updated docker build process to use nodejs debian/ubuntu repos --- deploy/docker/Dockerfile | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index b638e62c8..c618771a9 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -66,12 +66,17 @@ CMD [ "sh" , "/lowcoder/api-service/entrypoint.sh" ] ## FROM ubuntu:jammy as build-node-service -RUN apt update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y curl ca-certificates build-essential +RUN apt update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y curl ca-certificates build-essential gnupg + +# Add nodejs repo and keys +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list # Download nodejs and install yarn -RUN curl -sL https://deb.nodesource.com/setup_19.x | bash - \ -&& apt-get install --no-install-recommends -y nodejs \ -&& npm install -g yarn +RUN apt-get update \ + && apt-get install --no-install-recommends -y nodejs \ + && npm install -g yarn # Copy and build the node-service app COPY server/node-service/ /lowcoder/node-service/app/ @@ -93,9 +98,16 @@ RUN chmod +x /lowcoder/node-service/*.sh FROM ubuntu:jammy as lowcoder-ce-node-service LABEL maintainer="lowcoder" -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y curl ca-certificates \ - && curl -sL https://deb.nodesource.com/setup_19.x | bash - \ - && apt-get install --no-install-recommends -y nodejs gosu \ +RUN apt update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y curl ca-certificates gnupg + +# Add nodejs repo and keys +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list + +# Download nodejs and install yarn +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y nodejs gosu \ && npm install -g yarn \ && rm -rf /var/cache/apt/lists \ && addgroup --system --gid 9001 lowcoder \ @@ -167,13 +179,20 @@ EXPOSE 3443 FROM lowcoder-ce-frontend LABEL maintainer="lowcoder" +RUN apt update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y curl ca-certificates gnupg + +# Add nodejs repo and keys +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list + + # Install required packages RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y bash gnupg curl lsb-release \ && curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg \ && echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb bullseye main" | tee /etc/apt/sources.list.d/redis.list \ && curl -fsSL https://www.mongodb.org/static/pgp/server-4.4.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-archive-keyring.gpg \ && echo "deb [signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg arch=amd64,arm64] http://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list \ - && curl -sL https://deb.nodesource.com/setup_19.x | bash - \ && if [ "$(dpkg --print-architecture)" = "amd64" ] || [ "$(dpkg --print-architecture)" = "i386" ]; then \ curl -sL http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_$(dpkg --print-architecture).deb --output libssl1.1_1.1.1f-1ubuntu2_$(dpkg --print-architecture).deb; \ else \ From 6e1bc053d706e0968fc0972e656a418d0b384fb2 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Tue, 14 Nov 2023 14:24:09 +0500 Subject: [PATCH 10/24] fix: handle empty itemKeys for sub-menus --- .../comps/comps/layout/layoutMenuItemComp.tsx | 12 +++++++---- .../navComp/components/DroppableMenuItem.tsx | 20 ++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx index 62b55a7da..e7db2f29f 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx @@ -100,10 +100,14 @@ export class LayoutMenuItemListComp extends list(LayoutMenuItemCompMigrate) { const data = this.getView(); this.dispatch( this.pushAction( - value || { - label: trans("menuItem") + " " + (data.length + 1), - itemKey: genRandomKey(), - } + value + ? { + ...value, + itemKey: value.itemKey || genRandomKey(), + } : { + label: trans("menuItem") + " " + (data.length + 1), + itemKey: genRandomKey(), + } ) ); } diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx index 44c342101..4ba18b3dc 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx @@ -1,10 +1,12 @@ import { useDraggable, useDroppable } from "@dnd-kit/core"; import { trans } from "i18n"; -import { Fragment } from "react"; +import { Fragment, useEffect } from "react"; import styled from "styled-components"; import DroppablePlaceholder from "./DroppablePlaceHolder"; import MenuItem, { ICommonItemProps } from "./MenuItem"; import { IDragData, IDropData } from "./types"; +import { LayoutMenuItemComp } from "comps/comps/layout/layoutMenuItemComp"; +import { genRandomKey } from "comps/utils/idGenerator"; const DraggableMenuItemWrapper = styled.div` position: relative; @@ -63,6 +65,22 @@ export default function DraggableMenuItem(props: IDraggableMenuItemProps) { disabled: isDragging || disabled || disableDropIn, data: dropData, }); + + // TODO: Remove this later. + // Set ItemKey for previously added sub-menus + useEffect(() => { + if(!items.length) return; + if(!(items[0] instanceof LayoutMenuItemComp)) return; + + return items.forEach(item => { + const subItem = item as LayoutMenuItemComp; + const itemKey = subItem.children.itemKey.getView(); + if(itemKey === '') { + subItem.children.itemKey.dispatchChangeValueAction(genRandomKey()) + } + }) + }, [items]) + return ( <> From 48e4c713aa8863f365da611e8f3100c623312f0b Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Tue, 14 Nov 2023 18:26:41 +0500 Subject: [PATCH 11/24] feat: allow more than 2 levels nested nav --- .../src/comps/comps/layout/navLayout.tsx | 18 +++++++++++------- .../navComp/components/DroppableMenuItem.tsx | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 5e8d47320..f65125efe 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -133,21 +133,25 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { }, [filterItem] ); - - const itemKeyRecord = useMemo(() => { + + const generateItemKeyRecord = useCallback((items: LayoutMenuItemComp[]) => { + console.log('generateItemKeyRecord', items) const result: Record = {}; items.forEach((item) => { const subItems = item.children.items.getView(); if (subItems.length > 0) { - item.children.items - .getView() - .forEach((subItem) => (result[subItem.getItemKey()] = subItem)); - } else { + Object.assign(result, generateItemKeyRecord(subItems)) + } + else { result[item.getItemKey()] = item; } }); return result; - }, [items]); + }, [items]) + + const itemKeyRecord = useMemo(() => { + return generateItemKeyRecord(items) + }, [generateItemKeyRecord, items]); const defaultOpenKeys = useMemo(() => { let itemPath: string[]; diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx index 4ba18b3dc..c4f22191a 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx @@ -117,7 +117,7 @@ export default function DraggableMenuItem(props: IDraggableMenuItemProps) { item={subItem} level={0} disabled={disabled || isDragging || disableDropIn} - // onAddSubMenu={onAddSubMenu} + onAddSubMenu={onAddSubMenu} onDelete={onDelete} parentDragging={isDragging} /> From ee148f5babfba3d3c4a63349660131868a1e7a23 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Tue, 14 Nov 2023 18:55:29 +0500 Subject: [PATCH 12/24] feat: make parent menu items clickable --- .../comps/comps/layout/layoutMenuItemComp.tsx | 12 ++-- .../src/comps/comps/layout/navLayout.tsx | 58 +++++++++---------- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx index e7db2f29f..a833229a9 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx @@ -38,15 +38,13 @@ export class LayoutMenuItemComp extends MultiBaseComp { } override getPropertyView(): ReactNode { - const isLeaf = this.children.items.getView().length === 0; return ( <> - {isLeaf && - this.children.action.propertyView({ - onAppChange: (label) => { - label && this.children.label.dispatchChangeValueAction(label); - }, - })} + {this.children.action.propertyView({ + onAppChange: (label) => { + label && this.children.label.dispatchChangeValueAction(label); + }, + })} {this.children.label.propertyView({ label: trans("label") })} {this.children.icon.propertyView({ label: trans("icon"), diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index f65125efe..03920da75 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -71,6 +71,33 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { return !item.children.hidden.getView(); }, []); + const generateItemKeyRecord = useCallback((items: LayoutMenuItemComp[]) => { + const result: Record = {}; + items.forEach((item) => { + const subItems = item.children.items.getView(); + if (subItems.length > 0) { + Object.assign(result, generateItemKeyRecord(subItems)) + } + result[item.getItemKey()] = item; + }); + return result; + }, [items]) + + const itemKeyRecord = useMemo(() => { + return generateItemKeyRecord(items) + }, [generateItemKeyRecord, items]); + + const onMenuItemClick = ({key}: {key: string}) => { + const itemComp = itemKeyRecord[key]; + const url = [ + ALL_APPLICATIONS_URL, + pathParam.applicationId, + pathParam.viewMode, + itemComp.getItemKey(), + ].join("/"); + itemComp.children.action.act(url); + } + const getMenuItem = useCallback( (itemComps: LayoutMenuItemComp[]): MenuProps["items"] => { return itemComps.filter(filterItem).map((item) => { @@ -81,6 +108,8 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { title: label, key: item.getItemKey(), icon: {item.children.icon.getView()}, + onTitleClick: onMenuItemClick, + onClick: onMenuItemClick, ...(subItems.length > 0 && { children: getMenuItem(subItems) }), }; }); @@ -133,25 +162,6 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { }, [filterItem] ); - - const generateItemKeyRecord = useCallback((items: LayoutMenuItemComp[]) => { - console.log('generateItemKeyRecord', items) - const result: Record = {}; - items.forEach((item) => { - const subItems = item.children.items.getView(); - if (subItems.length > 0) { - Object.assign(result, generateItemKeyRecord(subItems)) - } - else { - result[item.getItemKey()] = item; - } - }); - return result; - }, [items]) - - const itemKeyRecord = useMemo(() => { - return generateItemKeyRecord(items) - }, [generateItemKeyRecord, items]); const defaultOpenKeys = useMemo(() => { let itemPath: string[]; @@ -192,16 +202,6 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { style={{ height: "100%" }} defaultOpenKeys={defaultOpenKeys} selectedKeys={[selectedKey]} - onClick={(e) => { - const itemComp = itemKeyRecord[e.key]; - const url = [ - ALL_APPLICATIONS_URL, - pathParam.applicationId, - pathParam.viewMode, - itemComp.getItemKey(), - ].join("/"); - itemComp.children.action.act(url); - }} /> {pageView} From 8e39c185b5a75c8ebfc672eba385b7b49caf4fb9 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Tue, 14 Nov 2023 22:34:23 +0500 Subject: [PATCH 13/24] feat: added width and background option for nav bar --- .../src/comps/comps/layout/navLayout.tsx | 21 +++++++++++++++++-- .../comps/controls/styleControlConstants.tsx | 2 ++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 03920da75..2a07f491e 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -8,12 +8,17 @@ import { withDispatchHook } from "comps/generators/withDispatchHook"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; import { ALL_APPLICATIONS_URL } from "constants/routesURL"; import { TopHeaderHeight } from "constants/style"; -import { Section } from "lowcoder-design"; +import { Section, sectionNames } from "lowcoder-design"; import { trans } from "i18n"; import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; import { useCallback, useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import { isUserViewMode, useAppPathParam } from "util/hooks"; +import { StringControl } from "comps/controls/codeControl"; +import { styleControl } from "comps/controls/styleControl"; +import { NavLayoutStyle } from "comps/controls/styleControlConstants"; + +const DEFAULT_WIDTH = 240; const StyledSide = styled(Layout.Sider)` max-height: calc(100vh - ${TopHeaderHeight}); @@ -46,6 +51,8 @@ let NavTmpLayout = (function () { label: trans("menuItem") + " 1", }, ]), + width: StringControl, + style: styleControl(NavLayoutStyle), }; return new MultiCompBuilder(childrenMap, (props) => { return null; @@ -54,6 +61,16 @@ let NavTmpLayout = (function () { return ( <>
{menuPropertyView(children.items)}
+
+ { children.width.propertyView({ + label: trans("drawer.width"), + tooltip: trans("drawer.widthTooltip"), + placeholder: DEFAULT_WIDTH + "", + })} +
+
+ { children.style.getPropertyView() } +
); }) @@ -195,7 +212,7 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { let content = ( - + Date: Thu, 16 Nov 2023 21:22:21 +0500 Subject: [PATCH 14/24] feat: styling option for nav layout --- .../src/comps/comps/layout/navLayout.tsx | 220 ++++++++++++++++-- .../comps/controls/styleControlConstants.tsx | 61 ++++- .../packages/lowcoder/src/i18n/locales/en.ts | 9 + .../packages/lowcoder/src/i18n/locales/zh.ts | 9 + 4 files changed, 272 insertions(+), 27 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 2a07f491e..afbdba8b8 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -1,4 +1,4 @@ -import { Layout, Menu as AntdMenu, MenuProps } from "antd"; +import { Layout, Menu as AntdMenu, MenuProps, Segmented } from "antd"; import MainContent from "components/layout/MainContent"; import { LayoutMenuItemComp, LayoutMenuItemListComp } from "comps/comps/layout/layoutMenuItemComp"; import { menuPropertyView } from "comps/comps/navComp/components/MenuItemList"; @@ -8,17 +8,47 @@ import { withDispatchHook } from "comps/generators/withDispatchHook"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; import { ALL_APPLICATIONS_URL } from "constants/routesURL"; import { TopHeaderHeight } from "constants/style"; -import { Section, sectionNames } from "lowcoder-design"; +import { Section, controlItem, sectionNames } from "lowcoder-design"; import { trans } from "i18n"; import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; import { useCallback, useEffect, useMemo, useState } from "react"; -import styled from "styled-components"; +import styled, { css } from "styled-components"; import { isUserViewMode, useAppPathParam } from "util/hooks"; import { StringControl } from "comps/controls/codeControl"; import { styleControl } from "comps/controls/styleControl"; -import { NavLayoutStyle } from "comps/controls/styleControlConstants"; +import { + NavLayoutStyle, + NavLayoutItemStyle, + NavLayoutItemStyleType, + NavLayoutItemHoverStyle, + NavLayoutItemHoverStyleType, + NavLayoutItemActiveStyle, + NavLayoutItemActiveStyleType, +} from "comps/controls/styleControlConstants"; +import { dropdownControl } from "comps/controls/dropdownControl"; const DEFAULT_WIDTH = 240; +const ModeOptions = [ + { label: trans("navLayout.modeInline"), value: "inline" }, + { label: trans("navLayout.modeVertical"), value: "vertical" }, +] as const; + +type MenuItemStyleOptionValue = "normal" | "hover" | "active"; + +const menuItemStyleOptions = [ + { + value: "normal", + label: "Normal", + }, + { + value: "hover", + label: "Hover", + }, + { + value: "active", + label: "Active", + } +] const StyledSide = styled(Layout.Sider)` max-height: calc(100vh - ${TopHeaderHeight}); @@ -44,6 +74,80 @@ const ContentWrapper = styled.div` } `; +const StyledMenu = styled(AntdMenu)<{ + $navItemStyle?: NavLayoutItemStyleType & { width: string}, + $navItemHoverStyle?: NavLayoutItemHoverStyleType, + $navItemActiveStyle?: NavLayoutItemActiveStyleType, +}>` + .ant-menu-item { + height: auto; + width: ${(props) => props.$navItemStyle?.width}; + background-color: ${(props) => props.$navItemStyle?.background}; + color: ${(props) => props.$navItemStyle?.text}; + border-radius: ${(props) => props.$navItemStyle?.radius} !important; + border: ${(props) => `1px solid ${props.$navItemStyle?.border}`}; + margin: ${(props) => props.$navItemStyle?.margin}; + padding: ${(props) => props.$navItemStyle?.padding}; + + } + .ant-menu-item-active { + background-color: ${(props) => props.$navItemHoverStyle?.background} !important; + color: ${(props) => props.$navItemHoverStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemHoverStyle?.border}`}; + } + + .ant-menu-item-selected { + background-color: ${(props) => props.$navItemActiveStyle?.background} !important; + color: ${(props) => props.$navItemActiveStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemActiveStyle?.border}`}; + } + + .ant-menu-submenu { + margin: ${(props) => props.$navItemStyle?.margin}; + width: ${(props) => props.$navItemStyle?.width}; + + .ant-menu-submenu-title { + width: 100%; + height: auto !important; + background-color: ${(props) => props.$navItemStyle?.background}; + color: ${(props) => props.$navItemStyle?.text}; + border-radius: ${(props) => props.$navItemStyle?.radius} !important; + border: ${(props) => `1px solid ${props.$navItemStyle?.border}`}; + margin: 0; + padding: ${(props) => props.$navItemStyle?.padding}; + + } + + .ant-menu-item { + width: 100%; + } + + &.ant-menu-submenu-active { + >.ant-menu-submenu-title { + width: 100%; + background-color: ${(props) => props.$navItemHoverStyle?.background} !important; + color: ${(props) => props.$navItemHoverStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemHoverStyle?.border}`}; + } + } + &.ant-menu-submenu-selected { + >.ant-menu-submenu-title { + width: 100%; + background-color: ${(props) => props.$navItemActiveStyle?.background} !important; + color: ${(props) => props.$navItemActiveStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemActiveStyle?.border}`}; + } + } + } + +`; + +const defaultStyle = { + radius: '0px', + margin: '0px', + padding: '0px', +} + let NavTmpLayout = (function () { const childrenMap = { items: withDefault(LayoutMenuItemListComp, [ @@ -51,27 +155,57 @@ let NavTmpLayout = (function () { label: trans("menuItem") + " 1", }, ]), - width: StringControl, - style: styleControl(NavLayoutStyle), + width: withDefault(StringControl, DEFAULT_WIDTH), + mode: dropdownControl(ModeOptions, "inline"), + navStyle: withDefault(styleControl(NavLayoutStyle), defaultStyle), + navItemStyle: withDefault(styleControl(NavLayoutItemStyle), defaultStyle), + navItemHoverStyle: withDefault(styleControl(NavLayoutItemHoverStyle), {}), + navItemActiveStyle: withDefault(styleControl(NavLayoutItemActiveStyle), {}), }; return new MultiCompBuilder(childrenMap, (props) => { return null; }) .setPropertyViewFn((children) => { + const [styleSegment, setStyleSegment] = useState('normal') + return ( - <> +
{menuPropertyView(children.items)}
{ children.width.propertyView({ - label: trans("drawer.width"), - tooltip: trans("drawer.widthTooltip"), + label: trans("navLayout.width"), + tooltip: trans("navLayout.widthTooltip"), placeholder: DEFAULT_WIDTH + "", - })} + })} + { children.mode.propertyView({ + label: trans("labelProp.position"), + radioButton: true + })}
-
- { children.style.getPropertyView() } +
+ { children.navStyle.getPropertyView() }
- +
+ {controlItem({}, ( + setStyleSegment(k as MenuItemStyleOptionValue)} + /> + ))} + {styleSegment === 'normal' && ( + children.navItemStyle.getPropertyView() + )} + {styleSegment === 'hover' && ( + children.navItemHoverStyle.getPropertyView() + )} + {styleSegment === 'active' && ( + children.navItemActiveStyle.getPropertyView() + )} +
+
); }) .build(); @@ -82,13 +216,19 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { const isViewMode = isUserViewMode(pathParam); const [selectedKey, setSelectedKey] = useState(""); const items = useMemo(() => comp.children.items.getView(), [comp.children.items]); - + const navWidth = useMemo(() => comp.children.width.getView(), [comp.children.width]); + const navMode = useMemo(() => comp.children.mode.getView(), [comp.children.mode]); + const navStyle = useMemo(() => comp.children.navStyle.getView(), [comp.children.navStyle]); + const navItemStyle = useMemo(() => comp.children.navItemStyle.getView(), [comp.children.navItemStyle]); + const navItemHoverStyle = useMemo(() => comp.children.navItemHoverStyle.getView(), [comp.children.navItemHoverStyle]); + const navItemActiveStyle = useMemo(() => comp.children.navItemActiveStyle.getView(), [comp.children.navItemActiveStyle]); + console.log(navItemActiveStyle); // filter out hidden. unauthorised items filtered by server const filterItem = useCallback((item: LayoutMenuItemComp): boolean => { return !item.children.hidden.getView(); }, []); - const generateItemKeyRecord = useCallback((items: LayoutMenuItemComp[]) => { + const generateItemKeyRecord = (items: LayoutMenuItemComp[]) => { const result: Record = {}; items.forEach((item) => { const subItems = item.children.items.getView(); @@ -98,13 +238,13 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { result[item.getItemKey()] = item; }); return result; - }, [items]) + } const itemKeyRecord = useMemo(() => { return generateItemKeyRecord(items) - }, [generateItemKeyRecord, items]); + }, [items]); - const onMenuItemClick = ({key}: {key: string}) => { + const onMenuItemClick = useCallback(({key}: {key: string}) => { const itemComp = itemKeyRecord[key]; const url = [ ALL_APPLICATIONS_URL, @@ -113,7 +253,7 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { itemComp.getItemKey(), ].join("/"); itemComp.children.action.act(url); - } + }, [pathParam.applicationId, pathParam.viewMode, itemKeyRecord]) const getMenuItem = useCallback( (itemComps: LayoutMenuItemComp[]): MenuProps["items"] => { @@ -131,7 +271,7 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { }; }); }, - [filterItem] + [onMenuItemClick, filterItem] ); const menuItems = useMemo(() => getMenuItem(items), [items, getMenuItem]); @@ -210,15 +350,47 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { } } + const getVerticalMargin = (margin: string[]) => { + if(margin.length === 1) return `${margin[0]}`; + if(margin.length === 2) return `(${margin[0]} + ${margin[0]})`; + if(margin.length === 3 || margin.length === 4) + return `(${margin[0]} + ${margin[2]})`; + + return '0px'; + } + const getHorizontalMargin = (margin: string[]) => { + if(margin.length === 1) return `(${margin[0]} + ${margin[0]})`; + if(margin.length === 2) return `(${margin[1]} + ${margin[1]})`; + if(margin.length === 3 || margin.length === 4) + return `(${margin[1]} + ${margin[3]})`; + + return '0px'; + } + let content = ( - - + {pageView} diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 715db926e..58b706771 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -821,9 +821,7 @@ export const TreeStyle = [ export const TreeSelectStyle = [...multiSelectCommon, ...ACCENT_VALIDATE] as const; -export const DrawerStyle = [getBackground()] as const; - -export const NavLayoutStyle = [getBackground()] as const; +export const DrawerStyle = [getBackground()] as const export const JsonEditorStyle = [LABEL] as const; @@ -930,6 +928,59 @@ export const ResponsiveLayoutColStyle = [ PADDING, ] as const; +export const NavLayoutStyle = [ + ...getBgBorderRadiusByBg(), + { + name: "text", + label: trans("text"), + depName: "background", + // depTheme: "primary", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, + MARGIN, + PADDING, +] as const; + +export const NavLayoutItemStyle = [ + getBackground("primarySurface"), + getStaticBorder('transparent'), + RADIUS, + { + name: "text", + label: trans("text"), + depName: "background", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, + MARGIN, + PADDING, +] as const; + +export const NavLayoutItemHoverStyle = [ + getBackground("canvas"), + getStaticBorder('transparent'), + { + name: "text", + label: trans("text"), + depName: "background", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, +] as const; + +export const NavLayoutItemActiveStyle = [ + getBackground("primary"), + getStaticBorder('transparent'), + { + name: "text", + label: trans("text"), + depName: "background", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, +] as const; + export const CarouselStyle = [getBackground("canvas")] as const; export const RichTextEditorStyle = [getStaticBorder(), RADIUS] as const; @@ -970,6 +1021,10 @@ export type CarouselStyleType = StyleConfigType; export type RichTextEditorStyleType = StyleConfigType; export type ResponsiveLayoutRowStyleType = StyleConfigType; export type ResponsiveLayoutColStyleType = StyleConfigType; +export type NavLayoutStyleType = StyleConfigType; +export type NavLayoutItemStyleType = StyleConfigType; +export type NavLayoutItemHoverStyleType = StyleConfigType; +export type NavLayoutItemActiveStyleType = StyleConfigType; export function widthCalculator(margin: string) { const marginArr = margin?.trim().replace(/\s+/g,' ').split(" ") || ""; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 40138fb17..2ade13c21 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -2710,4 +2710,13 @@ export const en = { rowLayout: "Row Layout", columnsLayout: "Columns Layout", }, + navLayout: { + mode: "Mode", + modeInline: "Inline", + modeVertical: "Vertical", + width: "Width", + widthTooltip: "Number or percentage, e.g. 520, 60%", + navStyle: "Menu Style", + navItemStyle: "Menu Item Style", + } }; diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 53b922a9e..aabf64e82 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -2557,6 +2557,15 @@ timeLine: { matchColumnsHeight: "匹配列高度", rowLayout: "行布局", columnsLayout: "栏目布局", + }, + navLayout: { + mode: "模式", + modeInline: "排队", + modeVertical: "垂直的", + width: "宽度", + widthTooltip: "数字或百分比,例如 520,60%", + navStyle: "菜单风格", + navItemStyle: "菜单项样式", } }; From 4be209ba6dc87856592209d1c1fa547bf88e4016 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Fri, 17 Nov 2023 00:54:00 +0500 Subject: [PATCH 15/24] feat: background-image for nav layout --- .../src/comps/comps/layout/navLayout.tsx | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index afbdba8b8..c69c8c294 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -26,6 +26,7 @@ import { NavLayoutItemActiveStyleType, } from "comps/controls/styleControlConstants"; import { dropdownControl } from "comps/controls/dropdownControl"; +import _ from "lodash"; const DEFAULT_WIDTH = 240; const ModeOptions = [ @@ -156,6 +157,7 @@ let NavTmpLayout = (function () { }, ]), width: withDefault(StringControl, DEFAULT_WIDTH), + backgroundImage: withDefault(StringControl, ""), mode: dropdownControl(ModeOptions, "inline"), navStyle: withDefault(styleControl(NavLayoutStyle), defaultStyle), navItemStyle: withDefault(styleControl(NavLayoutItemStyle), defaultStyle), @@ -170,7 +172,9 @@ let NavTmpLayout = (function () { return (
-
{menuPropertyView(children.items)}
+
+ {menuPropertyView(children.items)} +
{ children.width.propertyView({ label: trans("navLayout.width"), @@ -181,6 +185,10 @@ let NavTmpLayout = (function () { label: trans("labelProp.position"), radioButton: true })} + {children.backgroundImage.propertyView({ + label: `Background Image`, + placeholder: 'https://temp.im/350x400', + })}
{ children.navStyle.getPropertyView() } @@ -222,7 +230,8 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { const navItemStyle = useMemo(() => comp.children.navItemStyle.getView(), [comp.children.navItemStyle]); const navItemHoverStyle = useMemo(() => comp.children.navItemHoverStyle.getView(), [comp.children.navItemHoverStyle]); const navItemActiveStyle = useMemo(() => comp.children.navItemActiveStyle.getView(), [comp.children.navItemActiveStyle]); - console.log(navItemActiveStyle); + const backgroundImage = comp.children.backgroundImage.getView(); + // filter out hidden. unauthorised items filtered by server const filterItem = useCallback((item: LayoutMenuItemComp): boolean => { return !item.children.hidden.getView(); @@ -367,6 +376,10 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { return '0px'; } + let backgroundStyle = navStyle.background; + if(!_.isEmpty(backgroundImage)) { + backgroundStyle = `center / cover url('${backgroundImage}') no-repeat, ${backgroundStyle}`; + } let content = ( @@ -376,18 +389,18 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { style={{ height: `calc(100% - ${getVerticalMargin(navStyle.margin.split(' '))})`, width: `calc(100% - ${getHorizontalMargin(navStyle.margin.split(' '))})`, + borderRight: `1px solid ${navStyle.border}`, borderRadius: navStyle.radius, color: navStyle.text, margin: navStyle.margin, padding: navStyle.padding, - background: navStyle.background, - borderRight: `1px solid ${navStyle.border}`, + background: backgroundStyle, }} defaultOpenKeys={defaultOpenKeys} selectedKeys={[selectedKey]} $navItemStyle={{ width: `calc(100% - ${getHorizontalMargin(navItemStyle.margin.split(' '))})`, - ...navItemStyle + ...navItemStyle, }} $navItemHoverStyle={navItemHoverStyle} $navItemActiveStyle={navItemActiveStyle} From b06fa7ea7b9f26782a58c87d254a04b2bef22fb6 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Mon, 20 Nov 2023 19:38:50 +0500 Subject: [PATCH 16/24] feat: allow json data for nav layout --- .../comps/comps/layout/layoutMenuItemComp.tsx | 1 + .../src/comps/comps/layout/navLayout.tsx | 274 ++++++++++++++---- .../comps/comps/layout/navLayoutConstants.ts | 77 +++++ .../packages/lowcoder/src/i18n/locales/en.ts | 1 + .../packages/lowcoder/src/i18n/locales/zh.ts | 1 + 5 files changed, 299 insertions(+), 55 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts diff --git a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx index a833229a9..0999a4012 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx @@ -96,6 +96,7 @@ const LayoutMenuItemCompMigrate = migrateOldData(LayoutMenuItemComp, (oldData: a export class LayoutMenuItemListComp extends list(LayoutMenuItemCompMigrate) { addItem(value?: any) { const data = this.getView(); + this.dispatch( this.pushAction( value diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index c69c8c294..368e459a9 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -12,9 +12,9 @@ import { Section, controlItem, sectionNames } from "lowcoder-design"; import { trans } from "i18n"; import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; import { useCallback, useEffect, useMemo, useState } from "react"; -import styled, { css } from "styled-components"; +import styled from "styled-components"; import { isUserViewMode, useAppPathParam } from "util/hooks"; -import { StringControl } from "comps/controls/codeControl"; +import { StringControl, jsonControl } from "comps/controls/codeControl"; import { styleControl } from "comps/controls/styleControl"; import { NavLayoutStyle, @@ -27,30 +27,20 @@ import { } from "comps/controls/styleControlConstants"; import { dropdownControl } from "comps/controls/dropdownControl"; import _ from "lodash"; +import { check } from "util/convertUtils"; +import { genRandomKey } from "comps/utils/idGenerator"; +import history from "util/history"; +import { + DataOption, + DataOptionType, + ModeOptions, + jsonMenuItems, + menuItemStyleOptions +} from "./navLayoutConstants"; const DEFAULT_WIDTH = 240; -const ModeOptions = [ - { label: trans("navLayout.modeInline"), value: "inline" }, - { label: trans("navLayout.modeVertical"), value: "vertical" }, -] as const; - type MenuItemStyleOptionValue = "normal" | "hover" | "active"; -const menuItemStyleOptions = [ - { - value: "normal", - label: "Normal", - }, - { - value: "hover", - label: "Hover", - }, - { - value: "active", - label: "Active", - } -] - const StyledSide = styled(Layout.Sider)` max-height: calc(100vh - ${TopHeaderHeight}); overflow: auto; @@ -143,19 +133,57 @@ const StyledMenu = styled(AntdMenu)<{ `; +const StyledImage = styled.img` + height: 1em; + color: currentColor; +`; + const defaultStyle = { radius: '0px', margin: '0px', padding: '0px', } +type UrlActionType = { + url?: string; + newTab?: boolean; +} + +export type MenuItemNode = { + label: string; + key: string; + hidden?: boolean; + icon?: any; + action?: UrlActionType, + children?: MenuItemNode[]; +} + +function checkDataNodes(value: any, key?: string): MenuItemNode[] | undefined { + return check(value, ["array", "undefined"], key, (node, k) => { + check(node, ["object"], k); + check(node["label"], ["string"], "label"); + check(node["hidden"], ["boolean", "undefined"], "hidden"); + check(node["icon"], ["string", "undefined"], "icon"); + check(node["action"], ["object", "undefined"], "action"); + checkDataNodes(node["children"], "children"); + return node; + }); +} + +function convertTreeData(data: any) { + return data === "" ? [] : checkDataNodes(data) ?? []; +} + let NavTmpLayout = (function () { const childrenMap = { + dataOptionType: dropdownControl(DataOptionType, DataOption.Manual), items: withDefault(LayoutMenuItemListComp, [ { label: trans("menuItem") + " 1", + itemKey: genRandomKey(), }, ]), + jsonItems: jsonControl(convertTreeData, jsonMenuItems), width: withDefault(StringControl, DEFAULT_WIDTH), backgroundImage: withDefault(StringControl, ""), mode: dropdownControl(ModeOptions, "inline"), @@ -173,7 +201,17 @@ let NavTmpLayout = (function () { return (
- {menuPropertyView(children.items)} + {children.dataOptionType.propertyView({ + radioButton: true, + type: "oneline", + })} + { + children.dataOptionType.getView() === DataOption.Manual + ? menuPropertyView(children.items) + : children.jsonItems.propertyView({ + label: "Json Data", + }) + }
{ children.width.propertyView({ @@ -199,7 +237,6 @@ let NavTmpLayout = (function () { block options={menuItemStyleOptions} value={styleSegment} - // className="comp-panel-tab" onChange={(k) => setStyleSegment(k as MenuItemStyleOptionValue)} /> ))} @@ -223,46 +260,97 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { const pathParam = useAppPathParam(); const isViewMode = isUserViewMode(pathParam); const [selectedKey, setSelectedKey] = useState(""); - const items = useMemo(() => comp.children.items.getView(), [comp.children.items]); - const navWidth = useMemo(() => comp.children.width.getView(), [comp.children.width]); - const navMode = useMemo(() => comp.children.mode.getView(), [comp.children.mode]); - const navStyle = useMemo(() => comp.children.navStyle.getView(), [comp.children.navStyle]); - const navItemStyle = useMemo(() => comp.children.navItemStyle.getView(), [comp.children.navItemStyle]); - const navItemHoverStyle = useMemo(() => comp.children.navItemHoverStyle.getView(), [comp.children.navItemHoverStyle]); - const navItemActiveStyle = useMemo(() => comp.children.navItemActiveStyle.getView(), [comp.children.navItemActiveStyle]); + const items = comp.children.items.getView(); + const navWidth = comp.children.width.getView(); + const navMode = comp.children.mode.getView(); + const navStyle = comp.children.navStyle.getView(); + const navItemStyle = comp.children.navItemStyle.getView(); + const navItemHoverStyle = comp.children.navItemHoverStyle.getView(); + const navItemActiveStyle = comp.children.navItemActiveStyle.getView(); const backgroundImage = comp.children.backgroundImage.getView(); - + const jsonItems = comp.children.jsonItems.getView(); + const dataOptionType = comp.children.dataOptionType.getView(); + // filter out hidden. unauthorised items filtered by server const filterItem = useCallback((item: LayoutMenuItemComp): boolean => { return !item.children.hidden.getView(); }, []); - const generateItemKeyRecord = (items: LayoutMenuItemComp[]) => { - const result: Record = {}; - items.forEach((item) => { - const subItems = item.children.items.getView(); - if (subItems.length > 0) { - Object.assign(result, generateItemKeyRecord(subItems)) + const generateItemKeyRecord = useCallback( + (items: LayoutMenuItemComp[] | MenuItemNode[]) => { + const result: Record = {}; + if(dataOptionType === DataOption.Manual) { + (items as LayoutMenuItemComp[])?.forEach((item) => { + const subItems = item.children.items.getView(); + if (subItems.length > 0) { + Object.assign(result, generateItemKeyRecord(subItems)) + } + result[item.getItemKey()] = item; + }); } - result[item.getItemKey()] = item; - }); - return result; - } + if(dataOptionType === DataOption.Json) { + (items as MenuItemNode[])?.forEach((item) => { + if (item.children?.length) { + Object.assign(result, generateItemKeyRecord(item.children)) + } + result[item.key] = item; + }) + } + return result; + }, [dataOptionType] + ) const itemKeyRecord = useMemo(() => { + if(dataOptionType === DataOption.Json) { + return generateItemKeyRecord(jsonItems) + } return generateItemKeyRecord(items) - }, [items]); + }, [dataOptionType, jsonItems, items, generateItemKeyRecord]); const onMenuItemClick = useCallback(({key}: {key: string}) => { - const itemComp = itemKeyRecord[key]; + const itemComp = itemKeyRecord[key] + const url = [ ALL_APPLICATIONS_URL, pathParam.applicationId, pathParam.viewMode, - itemComp.getItemKey(), + key, ].join("/"); - itemComp.children.action.act(url); - }, [pathParam.applicationId, pathParam.viewMode, itemKeyRecord]) + + // handle manual menu item action + if(dataOptionType === DataOption.Manual) { + (itemComp as LayoutMenuItemComp).children.action.act(url); + return; + } + // handle json menu item action + if((itemComp as MenuItemNode).action?.newTab) { + return window.open((itemComp as MenuItemNode).action?.url, '_blank') + } + history.push(url); + }, [pathParam.applicationId, pathParam.viewMode, dataOptionType, itemKeyRecord]) + + const getJsonMenuItem = useCallback( + (items: MenuItemNode[]): MenuProps["items"] => { + return items?.map((item: MenuItemNode) => { + const { + label, + key, + hidden, + icon, + children, + } = item; + return { + label, + key, + hidden, + icon: , + onTitleClick: onMenuItemClick, + onClick: onMenuItemClick, + ...(children?.length && { children: getJsonMenuItem(children) }), + } + }) + }, [onMenuItemClick] + ) const getMenuItem = useCallback( (itemComps: LayoutMenuItemComp[]): MenuProps["items"] => { @@ -283,7 +371,11 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { [onMenuItemClick, filterItem] ); - const menuItems = useMemo(() => getMenuItem(items), [items, getMenuItem]); + const menuItems = useMemo(() => { + if(dataOptionType === DataOption.Json) return getJsonMenuItem(jsonItems) + + return getMenuItem(items) + }, [dataOptionType, jsonItems, getJsonMenuItem, items, getMenuItem]); // Find by path itemKey const findItemPathByKey = useCallback( @@ -329,7 +421,60 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { [filterItem] ); + // Find by path itemKey + const findItemPathByKeyJson = useCallback( + (itemComps: MenuItemNode[], itemKey: string): string[] => { + for (let item of itemComps) { + const subItems = item.children; + if (subItems?.length) { + // have subMenus + const childPath = findItemPathByKeyJson(subItems, itemKey); + if (childPath.length > 0) { + return [item.key, ...childPath]; + } + } else { + if (item.key === itemKey) { + return [item.key]; + } + } + } + return []; + }, + [] + ); + + // Get the first visible menu + const findFirstItemPathJson = useCallback( + (itemComps: MenuItemNode[]): string[] => { + for (let item of itemComps) { + if (!item.hidden) { + const subItems = item.children; + if (subItems?.length) { + // have subMenus + const childPath = findFirstItemPathJson(subItems); + if (childPath.length > 0) { + return [item.key, ...childPath]; + } + } else { + return [item.key]; + } + } + } + return []; + }, [] + ); + const defaultOpenKeys = useMemo(() => { + if(dataOptionType === DataOption.Json) { + let itemPath: string[]; + if (pathParam.appPageId) { + itemPath = findItemPathByKeyJson(jsonItems, pathParam.appPageId); + } else { + itemPath = findFirstItemPathJson(jsonItems); + } + return itemPath.slice(0, itemPath.length - 1); + } + let itemPath: string[]; if (pathParam.appPageId) { itemPath = findItemPathByKey(items, pathParam.appPageId); @@ -350,14 +495,32 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { setSelectedKey(selectedKey); }, [pathParam.appPageId]); - let pageView = ; - const selectedItem = itemKeyRecord[selectedKey]; - if (selectedItem && !selectedItem.children.hidden.getView()) { - const compView = selectedItem.children.action.getView(); - if (compView) { - pageView = compView; + const pageView = useMemo(() => { + let pageView = ; + + if(dataOptionType === DataOption.Manual) { + const selectedItem = (itemKeyRecord[selectedKey] as LayoutMenuItemComp); + if (selectedItem && !selectedItem.children.hidden.getView()) { + const compView = selectedItem.children.action.getView(); + if (compView) { + pageView = compView; + } + } } - } + if(dataOptionType === DataOption.Json) { + const item = (itemKeyRecord[selectedKey] as MenuItemNode) + if(item?.action?.url) { + pageView =