diff --git a/README.md b/README.md index 225c3f53d..b603176f7 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ <div align="center"> <h1 style="border-bottom: none; margin-bottom: 0">Lowcoder</h1> - <h3 style="margin-top: 0">The Open Source Retool, Tooljet and Appsmith Alternative</h3> + <h3 style="margin-top: 0">Lowcoder is the best Retool, Appsmith or Tooljet Alternative.</h3> <p> - Build internal and customer facing Apps fast, with no limitations + Create internal and external software applications for your Company and your Customers with minimal coding experience. </p> </div> -<img src="/docs/.gitbook/assets/Bu2fpz1h01.gif"/> +<img src="https://1167272343-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjNgeI0mUzgw6Re92iTOw%2Fuploads%2FnwXJC1XBqP2MvTQitPyo%2FApp%20Editor%20%7C%20Main%20Screeen%20clean.png?alt=media&token=e5fba81b-82a7-4c0e-a15d-baa781d5b13a"/> ## 📢 Use Lowcoder in 3 steps 1. Connect to any data sources or APIs. @@ -21,32 +21,42 @@ It's cumbersome to create a single app. You had to design user interfaces, write Low-code/No-code platforms are fast to get started with but quickly become unmaintainable and inflexible. This creates more problems than it solves. -Retool-like solutions are great for their simplicity and flexibility, but they can also be limited in different ways compared to frameworks like React/Vue. +NewGen Lowcode Platforms like Retool and others are great for their simplicity and flexibility - like Lowcoder too, but they can also be limited in different ways, especially when it comes to "external" applications for everyone. -Lowcoder wants to take a step forward. More specifically, Lowcoder is -- An all-in-one IDE to create internal or customer-facing apps. +Lowcoder wants to take a step forward. More specifically, Lowcoder is: +- An all-in-one IDE to create internal or customer-facing (external) apps. - A place to create, build and share building blocks of web applications. -- A domain-specific language that UI-configurable block is the first-class citizen. +- The tool and community to support your business, and lower the cost and time to develop interactive applications. +- The only platform to embed Lowcode Apps natively in Websites (no iFrame!) +- The only platform where you can build your own Meeting Tool - like Teams, Zoom or Google Meets, - just in the Lowcode way. ## 🪄 Features -- **Visual UI builder** with 50+ built-in components. +- **Visual UI builder** with 50+ built-in components. Save 90% of time to build apps. - **Modules** for reusable (!) component sets in the UI builder. -- **Embed Lowcoder Apps as native React component** instead of iFrame (!). [Demo](https://github.com/lowcoder-org/lowcoder-sdk-demo) +- **Embed Lowcoder Apps as native parts of any Website** instead of iFrame (!). [Demo](https://github.com/lowcoder-org/lowcoder-sdk-demo) +- **Video Meeting Components** to create your own individual Web-Meeting tool. - **Query Library** for reusable data queries of your data sources. - **Custom components** to develop own components and use them in the UI builder. - **Native Data connections** to PostgreSQL, MongoDB, MySQL, Redis, Elasticsearch, REST API, SMTP, etc. +- **Stream Data connections** to Websockets for realtime data updates & collaboration - **JavaScript supported everywhere** to transform data, control components, etc. - **Role-based access control (RBAC)** for granular permission management. - **Auto-saved and restorable history** for release and version control. - **App Themes and Theme Editor** to precisely align with your company's brand guidelines. -- **Self Hosting** to use Lowcoder in your internal company network. +- **Self Hosting** to use Lowcoder in your internal company network, even behind the firewall. - **Free Community Cloud** to start within a minute and build your first Apps. [Start here](https://app.lowcoder.cloud) ## 🏆 Comparisons +### Lowcoder vs Teams, Google Meets, Zoom +- build a Meeting tool with peace in mind. Blue buttons - ok. Red corners or circle Videostream - ok too. +- embedd applications in your Video-Meetings, so attendees can enjoy collaborative "anything". From shopping to working and gaming... +### Lowcoder vs Powerapps +- build a apps way faster than in Power Apps. Save up to 50& of the time at least. +- Use self-hosting to keep all apps and data under your control for example at the own baremetals. ### Lowcoder vs Retool - Lowcoder is open-source. You don't need to worry about vendor lock-in or being stuck with an outdated version of the software. -- In Lowcoder, developers can create and use their own components instead of depending on official updates. +- In Lowcoder, developers can build truly responsive apps - not as cumbersome as the "Desktop / Mobile switch" in Retool - Lowcoder is free and you can contribute! - The EE Version of Lowcoder comes with a much better pricing model, so you have no "per-user costs". ### Lowcoder vs Appsmith, Tooljet @@ -55,15 +65,17 @@ Lowcoder wants to take a step forward. More specifically, Lowcoder is - In Lowcoder, you can reuse common structures when building apps with modules and query library features. ### Lowcoder vs Mendix, Outsystems, Pega - Lowcoder is modern. The codebase is fresh and uses modern standards. -- Lowcoder Apps do not need a compile and deployment. Just publish and use. +- Lowcoder Apps do not need a compile and deployment. Just publish and use. Within seconds! - Lowcoder Apps can get embedded natively in websites and apps, even in mobile apps. ### Lowcoder vs internal Tool platforms - Lowcoder supports internal tools like admin panels perfectly, but also customer-facing apps can get developed and published. - The Lowcoder UI builder is straightforward and better to use than Bubble. - App release cycles and updates can be done nearly daily without service downtimes for customers and users. + ## 👐 Support and Community If you have any questions, please feel free to contact us or share them with our community. Our team is here ready to help. +And we mean it... Day by day! 📮 Best way is to chat with us on [Discord](https://discord.gg/qMG9uTmAx2) @@ -72,10 +84,16 @@ If you have any questions, please feel free to contact us or share them with our 🔎 Submit an issue here on [GitHub](https://github.com/lowcoder-org/lowcoder/issues) ## 💻 Deployment Options -You can access Lowcoder from [cloud-hosted version](https://www.lowcoder.cloud/) at any time, or use the following resources for deploying Lowcoder on different platforms: -- [Docker](docs/self-hosting/README.md) +You can access Lowcoder from [cloud-hosted version](https://app.lowcoder.cloud/) at any time, or use the following resources for deploying Lowcoder on different platforms: +- [Docker](https://docs.lowcoder.cloud/lowcoder-documentation/setup-and-run/self-hosting) ## 💪 Contributing - Language support: If you have experience with a language that isn't currently supported by our product, send us a pull request. - Create and share components or demos: If you've created something that might be useful to others, add the link here. - [Frontend contributing guide](https://github.com/lowcoder-org/lowcoder/tree/develop/client) + +## 🥇 Sponsors +Accelerate the growth of Lowcoder and unleash its potential with your Sponsorship – together, we're shaping the future of Lowcode for everyone! +[Be a Sponsor](https://github.com/sponsors/lowcoder-org) + +Like ... @CHSchuepfer. Thank you very much! \ No newline at end of file diff --git a/app.json b/app.json index 0889503a9..5d6a647c0 100644 --- a/app.json +++ b/app.json @@ -1,11 +1,16 @@ { "name": "lowcoder", - "description": "Lowcoder is a developer-friendly open-source low code platform to build internal apps within minutes.", + "description": "An all-in-one IDE to create internal or customer-facing apps. · Visual UI builder with 50+ built-in components", "repository": "https://github.com/lowcoder-org/lowcoder", - "logo": "https://cdn-files.openblocks.dev/logo.png", + "logo": "https://lowcoder.cloud/images/webclip.png", "keywords": [ - "low code", - "develop tool" + "LowCode", + "Low code", + "develop tool", + "Fast Application Development", + "Rapid development", + "Collaboration tool", + "Video conferencing" ], "stack": "container", "formation": { diff --git a/client/VERSION b/client/VERSION index cd57a8b95..9671f9a9b 100644 --- a/client/VERSION +++ b/client/VERSION @@ -1 +1 @@ -2.1.5 +2.1.7 \ No newline at end of file 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 }; diff --git a/client/packages/lowcoder-design/src/icons/icon-autocomplete-comp.svg b/client/packages/lowcoder-design/src/icons/icon-autocomplete-comp.svg index dd882963a..505c59adb 100644 --- a/client/packages/lowcoder-design/src/icons/icon-autocomplete-comp.svg +++ b/client/packages/lowcoder-design/src/icons/icon-autocomplete-comp.svg @@ -1 +1,20 @@ -<svg t="1690794551698" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="41410" width="200" height="200"><path d="M826.515614 829.65762H94.474185C42.378083 829.65762 0 787.273538 0 735.179436V288.816564C0 236.722462 42.378083 194.34038 94.474185 194.34038h835.05363c52.094102 0 94.474185 42.382083 94.474185 94.476184v326.288637c0 15.58003-12.634025 28.212055-28.212055 28.212055-15.58003 0-28.212055-12.634025-28.212055-28.212055V288.816564c0-20.982041-17.068033-38.050074-38.048075-38.050074H94.474185c-20.978041-0.002-38.048074 17.068033-38.048075 38.050074V735.181436c0 20.982041 17.068033 38.050074 38.048075 38.050074h732.041429c15.58003 0 28.212055 12.634025 28.212055 28.212055 0 15.58203-12.634025 28.214055-28.212055 28.214055z" fill="#d7d9e0" p-id="41411" data-spm-anchor-id="a313x.7781069.0.i11" class=""></path><path d="M913.357784 739.061443c-7.222014 0-14.440028-2.756005-19.950039-8.264016l-95.760187-95.756187c-11.018022-11.018022-11.018022-28.882056 0-39.902078 11.020022-11.014022 28.878056-11.014022 39.902078 0l95.760187 95.756187c11.018022 11.018022 11.018022 28.882056 0 39.902078a28.132055 28.132055 0 0 1-19.952039 8.264016z" fill="#d7d9e0" p-id="41412" data-spm-anchor-id="a313x.7781069.0.i13" class=""></path><path d="M749.529464 547.043068m-122.72024 0a122.72024 122.72024 0 1 0 245.44048 0 122.72024 122.72024 0 1 0-245.44048 0Z" fill="#3377ff" p-id="41413" data-spm-anchor-id="a313x.7781069.0.i15" class="selected"></path><path d="M749.539464 697.963363c-40.316079 0-78.220153-15.698031-106.726209-44.206086-28.506056-28.508056-44.206086-66.41013-44.206086-106.724209 0-40.316079 15.698031-78.220153 44.206086-106.726208 28.508056-28.506056 66.41013-44.206086 106.726209-44.206086s78.220153 15.698031 106.726208 44.206086c58.846115 58.846115 58.846115 154.602302 0 213.448417-28.508056 28.512056-66.41013 44.208086-106.726208 44.208086z m0-245.436479c-25.242049 0-48.974096 9.830019-66.828131 27.682054-17.850035 17.848035-27.682054 41.580081-27.682054 66.82813 0 25.242049 9.830019 48.974096 27.682054 66.824131 17.850035 17.850035 41.582081 27.682054 66.828131 27.682054 25.244049 0 48.974096-9.830019 66.82813-27.682054 36.848072-36.848072 36.848072-96.804189 0-133.650261-17.854035-17.854035-41.586081-27.684054-66.82813-27.684054zM513.349003 635.277241H183.258358c-15.58003 0-28.212055-12.634025-28.212055-28.212055 0-15.58003 12.634025-28.212055 28.212055-28.212055h330.088645c15.58003 0 28.212055 12.634025 28.212055 28.212055 0.002 15.58003-12.628025 28.212055-28.210055 28.212055z" fill="#d7d9e0" p-id="41414" data-spm-anchor-id="a313x.7781069.0.i12" class=""></path></svg> \ No newline at end of file +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 27.9.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#D7D9E0;} + .st1{fill:#3377FF;} +</style> +<path class="st0" d="M37.8,37.9H5.7c-2.3,0-4.1-1.9-4.1-4.1V14.2c0-2.3,1.9-4.1,4.1-4.1h36.6c2.3,0,4.1,1.9,4.1,4.1v14.3 + c0,0.7-0.6,1.2-1.2,1.2c-0.7,0-1.2-0.6-1.2-1.2V14.2c0-0.9-0.7-1.7-1.7-1.7H5.7c-0.9,0-1.7,0.7-1.7,1.7v19.6c0,0.9,0.7,1.7,1.7,1.7 + h32.1c0.7,0,1.2,0.6,1.2,1.2C39,37.4,38.5,37.9,37.8,37.9z"/> +<path class="st0" d="M41.6,34c-0.3,0-0.6-0.1-0.9-0.4l-4.2-4.2c-0.5-0.5-0.5-1.3,0-1.7c0.5-0.5,1.3-0.5,1.7,0l4.2,4.2 + c0.5,0.5,0.5,1.3,0,1.7C42.2,33.8,41.9,34,41.6,34L41.6,34z"/> +<path class="st1" d="M29,25.5c0,3,2.4,5.4,5.4,5.4s5.4-2.4,5.4-5.4s-2.4-5.4-5.4-5.4S29,22.6,29,25.5z"/> +<path class="st0" d="M34.4,32.2c-1.8,0-3.4-0.7-4.7-1.9c-1.2-1.2-1.9-2.9-1.9-4.7c0-1.8,0.7-3.4,1.9-4.7c1.2-1.2,2.9-1.9,4.7-1.9 + s3.4,0.7,4.7,1.9c2.6,2.6,2.6,6.8,0,9.4C37.8,31.5,36.2,32.2,34.4,32.2L34.4,32.2z M34.4,21.4c-1.1,0-2.1,0.4-2.9,1.2 + c-0.8,0.8-1.2,1.8-1.2,2.9c0,1.1,0.4,2.1,1.2,2.9c0.8,0.8,1.8,1.2,2.9,1.2c1.1,0,2.1-0.4,2.9-1.2c1.6-1.6,1.6-4.2,0-5.9 + C36.5,21.8,35.5,21.4,34.4,21.4L34.4,21.4z M24,29.4H9.6c-0.7,0-1.2-0.6-1.2-1.2c0-0.7,0.6-1.2,1.2-1.2H24c0.7,0,1.2,0.6,1.2,1.2 + C25.3,28.9,24.7,29.4,24,29.4z"/> +</svg> diff --git a/client/packages/lowcoder-design/src/icons/icon-comment-comp.svg b/client/packages/lowcoder-design/src/icons/icon-comment-comp.svg index b6828e6a0..5a737ee86 100644 --- a/client/packages/lowcoder-design/src/icons/icon-comment-comp.svg +++ b/client/packages/lowcoder-design/src/icons/icon-comment-comp.svg @@ -1 +1,13 @@ -<svg t="1690270225287" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5882" width="200" height="200"><path d="M781.3 188.6H269.7c-59.6 0-108 51.1-108 113.9v312.8c0 64.8 50.3 119.5 109.9 119.5h120.2c30.4 32.8 109.8 117 109.8 117 6.2 6.7 14.9 10.5 23.9 10.5s17.6-3.8 24.6-11.3c0.6-0.7 62.1-72 105.6-116.2h123.7c59.6 0 110-54.7 110-119.5V302.6c-0.1-62.9-48.6-114-108.1-114z m-431.4 321c-25.2 0-45.6-20.4-45.6-45.6s20.4-45.6 45.6-45.6 45.6 20.4 45.6 45.6-20.4 45.6-45.6 45.6z m179.4 0c-25.2 0-45.6-20.4-45.6-45.6s20.4-45.6 45.6-45.6 45.6 20.4 45.6 45.6-20.4 45.6-45.6 45.6z m179.4 0c-25.2 0-45.6-20.4-45.6-45.6s20.4-45.6 45.6-45.6 45.6 20.4 45.6 45.6-20.4 45.6-45.6 45.6z" fill="#B2B2B2" p-id="5883"></path></svg> \ No newline at end of file +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 27.9.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 200 200" style="enable-background:new 0 0 200 200;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#4575F6;} +</style> +<path class="st0" d="M152.6,36.8H52.7c-11.6,0-21.1,10-21.1,22.2v61.1c0,12.7,9.8,23.3,21.5,23.3h23.5c5.9,6.4,21.4,22.9,21.4,22.9 + c1.2,1.3,2.9,2.1,4.7,2.1s3.4-0.7,4.8-2.2c0.1-0.1,12.1-14.1,20.6-22.7h24.2c11.6,0,21.5-10.7,21.5-23.3V59.1 + C173.7,46.8,164.2,36.8,152.6,36.8z M68.3,99.5c-4.9,0-8.9-4-8.9-8.9s4-8.9,8.9-8.9s8.9,4,8.9,8.9S73.3,99.5,68.3,99.5z M103.4,99.5 + c-4.9,0-8.9-4-8.9-8.9s4-8.9,8.9-8.9c4.9,0,8.9,4,8.9,8.9S108.3,99.5,103.4,99.5z M138.4,99.5c-4.9,0-8.9-4-8.9-8.9s4-8.9,8.9-8.9 + c4.9,0,8.9,4,8.9,8.9S143.3,99.5,138.4,99.5z"/> +</svg> diff --git a/client/packages/lowcoder-design/src/icons/icon-mention-comp.svg b/client/packages/lowcoder-design/src/icons/icon-mention-comp.svg index 4c04c61e2..5b311e0b4 100644 --- a/client/packages/lowcoder-design/src/icons/icon-mention-comp.svg +++ b/client/packages/lowcoder-design/src/icons/icon-mention-comp.svg @@ -1 +1,20 @@ -<svg t="1690851406312" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2494" width="200" height="200"><path d="M981.333333 938.666667h-384A469.333333 469.333333 0 0 1 128 469.333333a384 384 0 0 1 768 0v192a64 64 0 0 1-128 0V256a42.666667 42.666667 0 0 0-85.333333 0 209.92 209.92 0 0 0-128-42.666667h-42.666667a256 256 0 0 0 0 512h42.666667a213.333333 213.333333 0 0 0 128-45.226666 148.906667 148.906667 0 0 0 298.666666-18.773334V469.333333a469.333333 469.333333 0 0 0-938.666666 0 554.666667 554.666667 0 0 0 554.666666 554.666667h384a42.666667 42.666667 0 0 0 0-85.333333z m-426.666666-298.666667h-42.666667a170.666667 170.666667 0 0 1 0-341.333333h42.666667a128 128 0 0 1 128 128v85.333333a128 128 0 0 1-128 128z" p-id="2495" fill="#d7d9e0"></path></svg> \ No newline at end of file +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 27.9.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#4575F6;} +</style> +<g> + <path class="st0" d="M14,38.5c-2.2-1-3.9-2.5-5-4.3c-1.1-1.9-1.7-4-1.7-6.6c0-3.6,0.9-6.9,2.6-9.9c1.8-3,4.2-5.3,7.2-7 + c3.1-1.7,6.5-2.5,10.2-2.5c4.3,0,7.7,1,10.2,2.9c2.5,1.9,3.8,4.6,3.8,8.1c0,2.5-0.5,4.7-1.6,6.8c-1.1,2.1-2.6,3.8-4.4,5 + s-3.9,1.8-6.1,1.8c-1.1,0-1.9-0.3-2.6-0.8c-0.7-0.5-1-1.2-1-2.2c-0.8,0.9-1.7,1.6-2.8,2.2c-1.1,0.5-2.3,0.8-3.5,0.8 + c-1.6,0-2.8-0.4-3.6-1.2c-0.8-0.8-1.2-2-1.2-3.5c0-2.1,0.6-4.2,1.7-6.2c1.1-2,2.6-3.7,4.3-4.9c1.8-1.3,3.6-1.9,5.6-1.9 + c1,0,1.8,0.2,2.5,0.7s1.1,1.1,1.4,1.9l1-2h3.6l-5.6,11.7c-0.2,0.4-0.3,0.7-0.3,1c0,0.6,0.4,0.8,1.3,0.8c1.2,0,2.3-0.5,3.4-1.4 + c1-0.9,1.9-2.1,2.5-3.6c0.6-1.5,0.9-3.1,0.9-4.8c0-2.6-0.9-4.5-2.7-5.7c-1.8-1.2-4.3-1.8-7.4-1.8c-2.9,0-5.5,0.6-7.7,1.9 + c-2.2,1.3-4,3-5.2,5.3c-1.2,2.3-1.8,4.9-1.8,7.8c0,2.9,0.9,5.1,2.7,6.6c1.8,1.5,4.5,2.3,8,2.3c1.8,0,3.4-0.2,4.9-0.5 + c1.5-0.3,2.9-0.9,4.2-1.7c0.5-0.2,0.9-0.4,1.2-0.4c0.5,0,1,0.2,1.4,0.5s0.7,0.8,1,1.4c-1.8,1.6-3.9,2.8-6.2,3.5 + C27,39.6,24.6,40,22.1,40C18.9,40,16.2,39.5,14,38.5z M23.6,28c1-1,1.9-2.2,2.6-3.6c0.7-1.4,1.1-2.8,1.1-4.1c0-1.2-0.6-1.9-1.9-1.9 + c-1,0-2.1,0.5-3.1,1.4c-1.1,0.9-2,2.1-2.7,3.5s-1.1,2.8-1.1,4.1c0,1.3,0.7,2,2.2,2C21.6,29.4,22.6,28.9,23.6,28z"/> +</g> +</svg> diff --git a/client/packages/lowcoder-design/src/icons/icon-timeline-comp.svg b/client/packages/lowcoder-design/src/icons/icon-timeline-comp.svg index 329690d6d..e47f5fc79 100644 --- a/client/packages/lowcoder-design/src/icons/icon-timeline-comp.svg +++ b/client/packages/lowcoder-design/src/icons/icon-timeline-comp.svg @@ -1 +1,23 @@ -<svg t="1689814019529" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2710" data-spm-anchor-id="a313x.7781069.0.i26" width="200" height="200"><path d="M896 618.666667H443.733333c-10.666667 0-21.333333-4.266667-29.866666-12.8l-78.933334-78.933334c-8.533333-8.533333-8.533333-21.333333 0-29.866666l78.933334-78.933334c8.533333-8.533333 19.2-12.8 29.866666-12.8H896c12.8 0 21.333333 8.533333 21.333333 21.333334v170.666666c0 12.8-8.533333 21.333333-21.333333 21.333334z" fill="#3377ff" p-id="2711" data-spm-anchor-id="a313x.7781069.0.i22" class="selected"></path><path d="M192 128h42.666667v768H192z" fill="#CFD8DC" p-id="2712"></path><path d="M213.333333 213.333333m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#90A4AE" p-id="2713"></path><path d="M213.333333 512m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#90A4AE" p-id="2714"></path><path d="M213.333333 810.666667m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#90A4AE" p-id="2715"></path><path d="M725.333333 917.333333H443.733333c-10.666667 0-21.333333-4.266667-29.866666-12.8l-78.933334-78.933333c-8.533333-8.533333-8.533333-21.333333 0-29.866667l78.933334-78.933333c8.533333-8.533333 19.2-12.8 29.866666-12.8H725.333333c12.8 0 21.333333 8.533333 21.333334 21.333333v170.666667c0 12.8-8.533333 21.333333-21.333334 21.333333z" fill="#dbdbdb" p-id="2716" data-spm-anchor-id="a313x.7781069.0.i23" class=""></path><path d="M746.666667 320H443.733333c-10.666667 0-21.333333-4.266667-29.866666-12.8l-78.933334-78.933333c-8.533333-8.533333-8.533333-21.333333 0-29.866667l78.933334-78.933333c8.533333-8.533333 19.2-12.8 29.866666-12.8H746.666667c12.8 0 21.333333 8.533333 21.333333 21.333333v170.666667c0 12.8-8.533333 21.333333-21.333333 21.333333z" fill="#dbdbdb" p-id="2717" data-spm-anchor-id="a313x.7781069.0.i24" class=""></path></svg> \ No newline at end of file +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 27.9.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#3377FF;} + .st1{fill:#CFD8DC;} + .st2{fill:#90A4AE;} + .st3{fill:#DBDBDB;} +</style> +<path class="st0" d="M43.6,29.5H20.4c-0.5,0-1.1-0.2-1.5-0.7l-4-4c-0.4-0.4-0.4-1.1,0-1.5l4-4c0.4-0.4,1-0.7,1.5-0.7h23.2 + c0.7,0,1.1,0.4,1.1,1.1v8.7C44.7,29,44.2,29.5,43.6,29.5z"/> +<path class="st1" d="M7.5,4.3h2.2v39.3H7.5V4.3z"/> +<path class="st2" d="M5.3,8.7c0,1.8,1.5,3.3,3.3,3.3c1.8,0,3.3-1.5,3.3-3.3c0,0,0,0,0,0c0-1.8-1.5-3.3-3.3-3.3S5.3,6.9,5.3,8.7 + L5.3,8.7z"/> +<path class="st2" d="M5.3,24c0,1.8,1.5,3.3,3.3,3.3s3.3-1.5,3.3-3.3c0,0,0,0,0,0c0-1.8-1.5-3.3-3.3-3.3S5.3,22.2,5.3,24L5.3,24z"/> +<path class="st2" d="M5.3,39.3c0,1.8,1.5,3.3,3.3,3.3s3.3-1.5,3.3-3.3c0,0,0,0,0,0c0-1.8-1.5-3.3-3.3-3.3S5.3,37.5,5.3,39.3 + L5.3,39.3z"/> +<path class="st3" d="M34.8,44.8H20.4c-0.5,0-1.1-0.2-1.5-0.7l-4-4c-0.4-0.4-0.4-1.1,0-1.5l4-4c0.4-0.4,1-0.7,1.5-0.7h14.4 + c0.7,0,1.1,0.4,1.1,1.1v8.7C35.9,44.3,35.5,44.8,34.8,44.8z"/> +<path class="st3" d="M35.9,14.2H20.4c-0.5,0-1.1-0.2-1.5-0.7l-4-4c-0.4-0.4-0.4-1.1,0-1.5l4-4c0.4-0.4,1-0.7,1.5-0.7h15.5 + c0.7,0,1.1,0.4,1.1,1.1v8.7C37,13.7,36.6,14.2,35.9,14.2L35.9,14.2z"/> +</svg> diff --git a/client/packages/lowcoder-design/src/icons/icon-undo.svg b/client/packages/lowcoder-design/src/icons/icon-undo.svg index e7296453f..a1199d1bb 100644 --- a/client/packages/lowcoder-design/src/icons/icon-undo.svg +++ b/client/packages/lowcoder-design/src/icons/icon-undo.svg @@ -1,3 +1,10 @@ -<svg t="1675856776421" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2686" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"> - <path d="M70.494 316.086c-3.484 3.173-3.484 8.656 0 11.829l193.119 175.883c5.137 4.679 13.387 1.034 13.387-5.915V368h395c114.875 0 208 93.125 208 208 0 114.876-93.125 208-208 208H256v80h416c159.058 0 288-128.942 288-288S831.058 288 672 288H277V146.117c0-6.948-8.25-10.593-13.387-5.914L70.494 316.086z" fill="#707070" p-id="2687"></path> -</svg> \ No newline at end of file +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 27.9.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#707070;} +</style> +<path class="st0" d="M3.4,15.4c-0.2,0.1-0.2,0.4,0,0.5l9,8.2c0.2,0.2,0.6,0,0.6-0.3v-6h18.4c5.3,0,9.7,4.3,9.7,9.7 + c0,5.3-4.3,9.7-9.7,9.7H12v3.7h19.3c7.4,0,13.4-6,13.4-13.4s-6-13.4-13.4-13.4H13V7.5c0-0.3-0.4-0.5-0.6-0.3L3.4,15.4z"/> +</svg> diff --git a/client/packages/lowcoder/site.webmanifest b/client/packages/lowcoder/site.webmanifest new file mode 100644 index 000000000..91352b1a9 --- /dev/null +++ b/client/packages/lowcoder/site.webmanifest @@ -0,0 +1 @@ +{"name":"Lowcoder.cloud","short_name":"Lowcoder","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} diff --git a/client/packages/lowcoder/src/assets/images/android-chrome-192x192.png b/client/packages/lowcoder/src/assets/images/android-chrome-192x192.png new file mode 100644 index 000000000..23d6e525e Binary files /dev/null and b/client/packages/lowcoder/src/assets/images/android-chrome-192x192.png differ diff --git a/client/packages/lowcoder/src/assets/images/android-chrome-512x512.png b/client/packages/lowcoder/src/assets/images/android-chrome-512x512.png new file mode 100644 index 000000000..c690d113c Binary files /dev/null and b/client/packages/lowcoder/src/assets/images/android-chrome-512x512.png differ diff --git a/client/packages/lowcoder/src/assets/images/apple-touch-icon.png b/client/packages/lowcoder/src/assets/images/apple-touch-icon.png new file mode 100644 index 000000000..6fff652aa Binary files /dev/null and b/client/packages/lowcoder/src/assets/images/apple-touch-icon.png differ diff --git a/client/packages/lowcoder/src/assets/images/favicon-16x16.png b/client/packages/lowcoder/src/assets/images/favicon-16x16.png new file mode 100644 index 000000000..a87cf442d Binary files /dev/null and b/client/packages/lowcoder/src/assets/images/favicon-16x16.png differ diff --git a/client/packages/lowcoder/src/assets/images/favicon-32x32.png b/client/packages/lowcoder/src/assets/images/favicon-32x32.png new file mode 100644 index 000000000..127dc44cb Binary files /dev/null and b/client/packages/lowcoder/src/assets/images/favicon-32x32.png differ diff --git a/client/packages/lowcoder/src/assets/images/favicon.ico b/client/packages/lowcoder/src/assets/images/favicon.ico index 91f699e41..91f1543d0 100644 Binary files a/client/packages/lowcoder/src/assets/images/favicon.ico and b/client/packages/lowcoder/src/assets/images/favicon.ico differ diff --git a/client/packages/lowcoder/src/components/CompName.tsx b/client/packages/lowcoder/src/components/CompName.tsx index cf63dbab4..2f0b26b8b 100644 --- a/client/packages/lowcoder/src/components/CompName.tsx +++ b/client/packages/lowcoder/src/components/CompName.tsx @@ -11,6 +11,7 @@ import { GreyTextColor } from "constants/style"; import { UICompType } from "comps/uiCompRegistry"; import { trans } from "i18n"; import { getComponentDocUrl } from "comps/utils/compDocUtil"; +import { getComponentPlaygroundUrl } from "comps/utils/compDocUtil"; import { parseCompType } from "comps/utils/remote"; const CompDiv = styled.div<{ width?: number; hasSearch?: boolean; showSearch?: boolean }>` @@ -78,6 +79,7 @@ export const CompName = (props: Iprops) => { const compType = selectedComp.children.compType.getView() as UICompType; const compInfo = parseCompType(compType); const docUrl = getComponentDocUrl(compType); + const playgroundUrl = getComponentPlaygroundUrl(compType); const items: EditPopoverItemType[] = []; @@ -99,6 +101,16 @@ export const CompName = (props: Iprops) => { }); } + if (playgroundUrl) { + items.push({ + text: trans("comp.menuViewPlayground"), + onClick: () => { + window.open(playgroundUrl, "_blank"); + }, + }); + } + + if (compInfo.isRemote) { items.push({ text: trans("comp.menuUpgradeToLatest"), diff --git a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx index 62b55a7da..0999a4012 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<ChildrenType> { } 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"), @@ -98,12 +96,17 @@ const LayoutMenuItemCompMigrate = migrateOldData(LayoutMenuItemComp, (oldData: a export class LayoutMenuItemListComp extends list(LayoutMenuItemCompMigrate) { addItem(value?: any) { 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/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 5e8d47320..368e459a9 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,12 +8,38 @@ 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, 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 { isUserViewMode, useAppPathParam } from "util/hooks"; +import { StringControl, jsonControl } from "comps/controls/codeControl"; +import { styleControl } from "comps/controls/styleControl"; +import { + NavLayoutStyle, + NavLayoutItemStyle, + NavLayoutItemStyleType, + NavLayoutItemHoverStyle, + NavLayoutItemHoverStyleType, + NavLayoutItemActiveStyle, + NavLayoutItemActiveStyleType, +} 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; +type MenuItemStyleOptionValue = "normal" | "hover" | "active"; const StyledSide = styled(Layout.Sider)` max-height: calc(100vh - ${TopHeaderHeight}); @@ -39,22 +65,192 @@ 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 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"), + 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 ( - <> - <Section name={trans("menu")}>{menuPropertyView(children.items)}</Section> - </> + <div style={{overflowY: 'auto'}}> + <Section name={trans("menu")}> + {children.dataOptionType.propertyView({ + radioButton: true, + type: "oneline", + })} + { + children.dataOptionType.getView() === DataOption.Manual + ? menuPropertyView(children.items) + : children.jsonItems.propertyView({ + label: "Json Data", + }) + } + </Section> + <Section name={sectionNames.layout}> + { children.width.propertyView({ + label: trans("navLayout.width"), + tooltip: trans("navLayout.widthTooltip"), + placeholder: DEFAULT_WIDTH + "", + })} + { children.mode.propertyView({ + label: trans("labelProp.position"), + radioButton: true + })} + {children.backgroundImage.propertyView({ + label: `Background Image`, + placeholder: 'https://temp.im/350x400', + })} + </Section> + <Section name={trans("navLayout.navStyle")}> + { children.navStyle.getPropertyView() } + </Section> + <Section name={trans("navLayout.navItemStyle")}> + {controlItem({}, ( + <Segmented + block + options={menuItemStyleOptions} + value={styleSegment} + onChange={(k) => setStyleSegment(k as MenuItemStyleOptionValue)} + /> + ))} + {styleSegment === 'normal' && ( + children.navItemStyle.getPropertyView() + )} + {styleSegment === 'hover' && ( + children.navItemHoverStyle.getPropertyView() + )} + {styleSegment === 'active' && ( + children.navItemActiveStyle.getPropertyView() + )} + </Section> + </div> ); }) .build(); @@ -64,13 +260,98 @@ 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 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 = useCallback( + (items: LayoutMenuItemComp[] | MenuItemNode[]) => { + const result: Record<string, LayoutMenuItemComp | MenuItemNode> = {}; + 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; + }); + } + 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) + }, [dataOptionType, jsonItems, items, generateItemKeyRecord]); + + const onMenuItemClick = useCallback(({key}: {key: string}) => { + const itemComp = itemKeyRecord[key] + + const url = [ + ALL_APPLICATIONS_URL, + pathParam.applicationId, + pathParam.viewMode, + key, + ].join("/"); + + // 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: <StyledImage src={icon} />, + onTitleClick: onMenuItemClick, + onClick: onMenuItemClick, + ...(children?.length && { children: getJsonMenuItem(children) }), + } + }) + }, [onMenuItemClick] + ) + const getMenuItem = useCallback( (itemComps: LayoutMenuItemComp[]): MenuProps["items"] => { return itemComps.filter(filterItem).map((item) => { @@ -81,14 +362,20 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { title: label, key: item.getItemKey(), icon: <span>{item.children.icon.getView()}</span>, + onTitleClick: onMenuItemClick, + onClick: onMenuItemClick, ...(subItems.length > 0 && { children: getMenuItem(subItems) }), }; }); }, - [filterItem] + [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( @@ -134,22 +421,60 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { [filterItem] ); - const itemKeyRecord = useMemo(() => { - const result: Record<string, LayoutMenuItemComp> = {}; - items.forEach((item) => { - const subItems = item.children.items.getView(); - if (subItems.length > 0) { - item.children.items - .getView() - .forEach((subItem) => (result[subItem.getItemKey()] = subItem)); - } else { - result[item.getItemKey()] = item; + // 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 result; - }, [items]); + 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); @@ -170,34 +495,79 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { setSelectedKey(selectedKey); }, [pathParam.appPageId]); - let pageView = <EmptyContent text="" style={{ height: "100%" }} />; - 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 = <EmptyContent text="" style={{ height: "100%" }} />; + + 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 = <iframe + title={item?.action?.url} + src={item?.action?.url} + width="100%" + height="100%" + style={{ border: "none", marginBottom: "-6px" }} + /> + } } + return pageView; + }, [dataOptionType, itemKeyRecord, selectedKey]) + + 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 backgroundStyle = navStyle.background; + if(!_.isEmpty(backgroundImage)) { + backgroundStyle = `center / cover url('${backgroundImage}') no-repeat, ${backgroundStyle}`; } let content = ( <Layout> - <StyledSide theme="light" width={240}> - <AntdMenu + <StyledSide theme="light" width={navWidth}> + <StyledMenu items={menuItems} - mode="inline" - style={{ height: "100%" }} + mode={navMode} + 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: backgroundStyle, + }} 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); + $navItemStyle={{ + width: `calc(100% - ${getHorizontalMargin(navItemStyle.margin.split(' '))})`, + ...navItemStyle, }} + $navItemHoverStyle={navItemHoverStyle} + $navItemActiveStyle={navItemActiveStyle} /> </StyledSide> <MainContent>{pageView}</MainContent> diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts new file mode 100644 index 000000000..e8fc23c0b --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts @@ -0,0 +1,77 @@ +import { trans } from "i18n"; + +export const ModeOptions = [ + { label: trans("navLayout.modeInline"), value: "inline" }, + { label: trans("navLayout.modeVertical"), value: "vertical" }, +] as const; + +export const DataOption = { + Manual: 'manual', + Json: 'json', +} +export const DataOptionType = [ + { + label: trans("prop.manual"), + value: DataOption.Manual, + }, + { + label: trans("prop.json"), + value: DataOption.Json, + }, +]; + +export const menuItemStyleOptions = [ + { + value: "normal", + label: "Normal", + }, + { + value: "hover", + label: "Hover", + }, + { + value: "active", + label: "Active", + } +]; + +export const jsonMenuItems = [ + { + label: "Menu Item 1", + key: 'menu-item-1', + icon: "https://cdn-icons-png.flaticon.com/128/149/149338.png", + action: { + url: "https://www.lowcoder.cloud", + newTab: false, + }, + children: [ + { + label: "Submenu Item 1", + key: 'submenu-item-11', + icon: "", + action: { + url: "https://www.lowcoder.cloud", + newTab: false, + }, + }, + { + label: "Submenu Item 2", + key: 'submenu-item-12', + icon: "", + action: { + url: "https://www.lowcoder.cloud", + newTab: false, + }, + }, + ] + }, + { + label: "Menu Item 2", + key: 'menu-item-2', + icon: "https://cdn-icons-png.flaticon.com/128/149/149206.png", + action: { + url: "https://www.lowcoder.cloud", + newTab: true, + }, + } +] \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx index 49a5f2cbb..be45aede9 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingControllerComp.tsx @@ -41,7 +41,7 @@ import { useUserViewMode } from "util/hooks"; import { isNumeric } from "util/stringUtils"; import { NameConfig, withExposingConfigs } from "../../generators/withExposing"; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from "uuid"; // import axios from "axios"; @@ -147,6 +147,7 @@ const shareScreen = async (sharing: boolean) => { try { if (sharing === false) { await client.unpublish(screenShareStream); + screenShareStream.close(); await client.publish(videoTrack); videoTrack.play(userId + ""); } else { @@ -165,11 +166,16 @@ const shareScreen = async (sharing: boolean) => { } }; const leaveChannel = async () => { + //stops local sharing video + screenShareStream.close(); + + //stops local video streaming and puts off the camera if (videoTrack) { await client.unpublish(videoTrack); await turnOnCamera(false); } + //mutes and stops locla audio stream if (audioTrack) { await turnOnMicrophone(false); } @@ -183,12 +189,12 @@ const publishVideo = async ( rtmToken: string, rtcToken: string ) => { - // 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); + // 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) => { @@ -231,8 +237,14 @@ export const meetingControllerChildren = { participants: stateComp<JSONValue>([]), usersScreenShared: stateComp<JSONValue>([]), localUser: jsonObjectExposingStateControl(""), - localUserID : withDefault(stringStateControl(trans("meeting.localUserID")), uuidv4() + ""), - meetingName: withDefault(stringStateControl(trans("meeting.meetingName")), uuidv4() + ""), + 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<JSONValue>([]), @@ -265,7 +277,8 @@ let MTComp = (function () { }); const [rtmMessages, setRtmMessages] = useState<any>([]); const [localUserSpeaking, setLocalUserSpeaking] = useState<any>(false); - const [localUserVideo, setLocalUserVideo] = useState<IAgoraRTCRemoteUser>(); + const [localUserVideo, setLocalUserVideo] = + useState<IAgoraRTCRemoteUser>(); const [userJoined, setUserJoined] = useState<IAgoraRTCRemoteUser>(); const [userLeft, setUserLeft] = useState<IAgoraRTCRemoteUser>(); @@ -323,6 +336,8 @@ let MTComp = (function () { } }, [userLeft]); + console.log("sharing", props.sharing); + useEffect(() => { if (updateVolume.userid) { let prevUsers: [] = props.participants as []; @@ -342,6 +357,28 @@ let MTComp = (function () { } }, [updateVolume]); + useEffect(() => { + let prevUsers: [] = props.participants as []; + const updatedItems = prevUsers.map((userInfo: any) => { + if (userInfo.user === localUserVideo?.uid) { + return { ...userInfo, streamingSharing: props.sharing.value }; + } + return userInfo; + }); + dispatch( + changeChildAction("participants", getData(updatedItems).data, false) + ); + + let localObject = { + user: userId + "", + audiostatus: props.audioControl.value, + streamingVideo: props.videoControl.value, + streamingSharing: props.sharing.value, + speaking: localUserSpeaking, + }; + props.localUser.onChange(localObject); + }, [props.sharing.value]); + useEffect(() => { let prevUsers: [] = props.participants as []; const updatedItems = prevUsers.map((userInfo: any) => { @@ -378,10 +415,33 @@ 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); + 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) ); @@ -391,19 +451,24 @@ let MTComp = (function () { useEffect(() => { if (client) { + //Enable Agora to send audio bytes client.enableAudioVolumeIndicator(); + //user activity listeners client.on("user-joined", (user: IAgoraRTCRemoteUser) => { setUserJoined(user); }); client.on("user-left", (user: IAgoraRTCRemoteUser, reason: any) => { setUserLeft(user); }); + + //listen to user speaking, client.on("volume-indicator", (volumeInfos: any) => { - if (volumeInfos.length == 0) return; + if (volumeInfos.length === 0) return; volumeInfos.map((volumeInfo: any) => { + //when the volume is above 30, user is probably speaking const speaking = volumeInfo.level >= 30; if ( - volumeInfo.uid == userId && + volumeInfo.uid === userId && props.localUser.value.speaking != speaking ) { setLocalUserSpeaking(speaking); @@ -521,8 +586,8 @@ let MTComp = (function () { })} </Section> <Section name={sectionNames.meetings}> - {children.appId.propertyView({ - label: trans("meeting.appid") + {children.appId.propertyView({ + label: trans("meeting.appid"), })} {children.meetingName.propertyView({ label: trans("meeting.meetingName"), @@ -633,7 +698,10 @@ MTComp = withMethodExposing(MTComp, [ }, execute: async (comp, values) => { if (comp.children.meetingActive.getView().value) return; - userId = comp.children.localUserID.getView().value === "" ? uuidv4() : comp.children.localUserID.getView().value; + userId = + comp.children.localUserID.getView().value === "" + ? uuidv4() + : comp.children.localUserID.getView().value; comp.children.localUser.change({ user: userId + "", audiostatus: false, @@ -656,7 +724,9 @@ MTComp = withMethodExposing(MTComp, [ comp.children.videoControl.change(true); await publishVideo( comp.children.appId.getView(), - comp.children.meetingName.getView().value === "" ? uuidv4() : comp.children.meetingName.getView().value, + comp.children.meetingName.getView().value === "" + ? uuidv4() + : comp.children.meetingName.getView().value, comp.children.rtmToken.getView().value, comp.children.rtcToken.getView().value ); @@ -671,21 +741,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); @@ -778,7 +847,7 @@ export const VideoMeetingControllerComp = withExposingConfigs(MTComp, [ new NameConfig("meetingActive", trans("meeting.meetingActive")), new NameConfig("meetingName", trans("meeting.meetingName")), new NameConfig("localUserID", trans("meeting.localUserID")), - new NameConfig("messages", trans("meeting.messages")), + 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 5fbc372b7..fe868117a 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingStreamComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/videoMeetingStreamComp.tsx @@ -137,7 +137,6 @@ const typeOptions = [ export const meetingStreamChildren = { autoHeight: withDefault(AutoHeightControl, "fixed"), - shareScreen: withDefault(BoolShareVideoControl, false), profilePadding: withDefault(StringControl, "0px"), profileBorderRadius: withDefault(StringControl, "0px"), videoAspectRatio: withDefault(StringControl, "1 / 1"), @@ -236,6 +235,9 @@ let VideoCompBuilder = (function (props) { } }, [props.userId.value]); + console.log(props.userId); + + return ( <EditorContext.Consumer> {(editorState) => ( @@ -264,7 +266,7 @@ let VideoCompBuilder = (function (props) { borderRadius: props.style.radius, width: "auto", }} - id={props.shareScreen ? "share-screen" : userId} + id={userId} ></VideoContainer> ) : ( <></> @@ -300,9 +302,6 @@ let VideoCompBuilder = (function (props) { <Section name={sectionNames.basic}> {children.userId.propertyView({ label: trans("meeting.videoId") })} {children.autoHeight.getPropertyView()} - {children.shareScreen.propertyView({ - label: trans("meeting.shareScreen"), - })} {children.profileImageUrl.propertyView({ label: trans("meeting.profileImageUrl"), placeholder: "https://via.placeholder.com/120", diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/videoSharingStreamComp.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/videoSharingStreamComp.tsx new file mode 100644 index 000000000..424d04853 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/videoSharingStreamComp.tsx @@ -0,0 +1,345 @@ +import { BoolCodeControl } from "comps/controls/codeControl"; +import { dropdownControl } from "comps/controls/dropdownControl"; +// import { IconControl } from "comps/controls/iconControl"; +import { CompNameContext, EditorContext, EditorState } from "comps/editorState"; +import { withDefault } from "comps/generators"; +import { UICompBuilder } from "comps/generators/uiCompBuilder"; +import ReactResizeDetector from "react-resize-detector"; +// import _ from "lodash"; +import { + CommonBlueLabel, + controlItem, + Dropdown, + Section, + sectionNames, +} from "lowcoder-design"; +import { trans } from "i18n"; + +import styled, { css } from "styled-components"; +import { + CommonNameConfig, + NameConfig, + withExposingConfigs, +} from "../../generators/withExposing"; +import { IForm } from "../formComp/formDataConstants"; +import { SimpleNameComp } from "../simpleNameComp"; +import { ButtonStyleControl } from "./videobuttonCompConstants"; +import { RefControl } from "comps/controls/refControl"; +import { useEffect, useRef, useState } from "react"; + +import { AutoHeightControl } from "comps/controls/autoHeightControl"; +import { client } from "./videoMeetingControllerComp"; + +import { IAgoraRTCRemoteUser } from "agora-rtc-sdk-ng"; + +import { + MeetingEventHandlerControl, + StringControl, + StringStateControl, + hiddenPropertyView, + stringExposingStateControl, +} from "@lowcoder-ee/index.sdk"; +import { BoolShareVideoControl } from "./meetingControlerUtils"; + +const FormLabel = styled(CommonBlueLabel)` + font-size: 13px; + margin-right: 4px; +`; + +function getFormOptions(editorState: EditorState) { + return editorState + .uiCompInfoList() + .filter((info) => info.type === "form") + .map((info) => ({ + label: info.name, + value: info.name, + })); +} + +const VideoContainer = styled.video` + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: space-around; +`; + +function getForm(editorState: EditorState, formName: string) { + const comp = editorState?.getUICompByName(formName); + if (comp && comp.children.compType.getView() === "form") { + return comp.children.comp as unknown as IForm; + } +} + +function getFormEventHandlerPropertyView( + editorState: EditorState, + formName: string +) { + const form = getForm(editorState, formName); + if (!form) { + return undefined; + } + + return ( + <CompNameContext.Provider value={formName}> + {form.onEventPropertyView( + <> + <FormLabel + onClick={() => + editorState.setSelectedCompNames( + new Set([formName]), + "rightPanel" + ) + } + > + {formName} + </FormLabel> + {trans("button.formButtonEvent")} + </> + )} + </CompNameContext.Provider> + ); +} + +class SelectFormControl extends SimpleNameComp { + override getPropertyView() { + const label = trans("button.formToSubmit"); + return controlItem( + { filterText: label }, + <EditorContext.Consumer> + {(editorState) => ( + <> + <Dropdown + label={label} + value={this.value} + options={getFormOptions(editorState)} + onChange={(value) => this.dispatchChangeValueAction(value)} + allowClear={true} + /> + {getFormEventHandlerPropertyView(editorState, this.value)} + </> + )} + </EditorContext.Consumer> + ); + } +} + +const typeOptions = [ + { + label: trans("button.default"), + value: "", + }, + { + label: trans("button.submit"), + value: "submit", + }, +] as const; + +export const meetingStreamChildren = { + autoHeight: withDefault(AutoHeightControl, "fixed"), + profilePadding: withDefault(StringControl, "0px"), + profileBorderRadius: withDefault(StringControl, "0px"), + videoAspectRatio: withDefault(StringControl, "1 / 1"), + type: dropdownControl(typeOptions, ""), + onEvent: MeetingEventHandlerControl, + disabled: BoolCodeControl, + loading: BoolCodeControl, + form: SelectFormControl, + // prefixIcon: IconControl, + // suffixIcon: IconControl, + style: ButtonStyleControl, + viewRef: RefControl<HTMLElement>, + userId: stringExposingStateControl(""), + 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"), +}; + +let SharingCompBuilder = (function (props) { + return new UICompBuilder(meetingStreamChildren, (props) => { + const videoRef = useRef<HTMLVideoElement>(null); + const conRef = useRef<HTMLDivElement>(null); + const [userId, setUserId] = useState(); + const [userName, setUsername] = useState(""); + const [showVideoSharing, setVideoSharing] = useState(true); + + useEffect(() => { + if (props.userId.value !== "") { + let userData = JSON.parse(props.userId?.value); + client.on( + "user-published", + async (user: IAgoraRTCRemoteUser, mediaType: "video" | "audio") => { + if (mediaType === "video") { + const remoteTrack = await client.subscribe(user, mediaType); + let userId = user.uid + ""; + if ( + user.hasVideo && + user.uid + "" !== userData.user && + userData.user !== "" + ) { + props.onEvent("videoOn"); + } + const element = document.getElementById(userId); + + if (element) { + remoteTrack.play(userId); + } + } + if (mediaType === "audio") { + const remoteTrack = await client.subscribe(user, mediaType); + if ( + user.hasAudio && + user.uid + "" !== userData.user && + userData.user !== "" + ) { + userData.audiostatus = user.hasVideo; + + props.onEvent("audioUnmuted"); + } + remoteTrack.play(); + } + } + ); + client.on( + "user-unpublished", + (user: IAgoraRTCRemoteUser, mediaType: "video" | "audio") => { + console.log("user-unpublished"); + + if (mediaType === "audio") { + if ( + !user.hasAudio && + user.uid + "" !== userData.user && + userData.user !== "" + ) { + userData.audiostatus = user.hasVideo; + props.onEvent("audioMuted"); + } + } + if (mediaType === "video") { + if (videoRef.current && videoRef.current?.id === user.uid + "") { + videoRef.current.srcObject = null; + } + if ( + !user.hasVideo && + user.uid + "" !== userData.user && + userData.user !== "" + ) { + props.onEvent("videoOff"); + } + } + } + ); + + setUserId(userData.user); + setUsername(userData.userName); + setVideoSharing(userData.streamingSharing); + } + }, [props.userId.value]); + + return ( + <EditorContext.Consumer> + {(editorState) => ( + <ReactResizeDetector> + <div + ref={conRef} + style={{ + display: "flex", + alignItems: "center", + height: "100%", + overflow: "hidden", + borderRadius: props.style.radius, + aspectRatio: props.videoAspectRatio, + backgroundColor: props.style.background, + padding: props.style.padding, + margin: props.style.margin, + }} + > + {userId ? ( + <VideoContainer + onClick={() => props.onEvent("videoClicked")} + ref={videoRef} + style={{ + display: `${showVideoSharing ? "flex" : "none"}`, + aspectRatio: props.videoAspectRatio, + borderRadius: props.style.radius, + width: "auto", + }} + id="share-screen" + ></VideoContainer> + ) : ( + <></> + )} + <div + style={{ + flexDirection: "column", + alignItems: "center", + display: `${!showVideoSharing || userId ? "flex" : "none"}`, + margin: "0 auto", + padding: props.profilePadding, + }} + > + <img + alt="" + style={{ + borderRadius: props.profileBorderRadius, + width: "100%", + overflow: "hidden", + }} + src={props.profileImageUrl.value} + /> + <p style={{ margin: "0" }}>{userName ?? ""}</p> + </div> + </div> + </ReactResizeDetector> + )} + </EditorContext.Consumer> + ); + }) + .setPropertyViewFn((children) => ( + <> + <Section name={sectionNames.basic}> + {children.userId.propertyView({ label: trans("meeting.videoId") })} + {children.autoHeight.getPropertyView()} + {children.profileImageUrl.propertyView({ + label: trans("meeting.profileImageUrl"), + placeholder: "https://via.placeholder.com/120", + })} + </Section> + + <Section name={sectionNames.interaction}> + {children.onEvent.getPropertyView()} + </Section> + <Section name={sectionNames.layout}> + {hiddenPropertyView(children)} + </Section> + <Section name={sectionNames.style}> + {children.profilePadding.propertyView({ + label: "Profile Image Padding", + })} + {children.profileBorderRadius.propertyView({ + label: "Profile Image Border Radius", + })} + {children.videoAspectRatio.propertyView({ + label: "Video Aspect Ratio", + })} + {children.style.getPropertyView()} + </Section> + </> + )) + .build(); +})(); + +SharingCompBuilder = class extends SharingCompBuilder { + override autoHeight(): boolean { + return this.children.autoHeight.getView(); + } +}; + +export const VideoSharingStreamComp = withExposingConfigs(SharingCompBuilder, [ + new NameConfig("loading", trans("button.loadingDesc")), + new NameConfig("profileImageUrl", trans("meeting.profileImageUrl")), + + ...CommonNameConfig, +]); 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..c4f22191a 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 ( <> <DraggableMenuItemWrapper> @@ -99,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} /> diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 0344f21c9..58b706771 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -821,7 +821,7 @@ export const TreeStyle = [ export const TreeSelectStyle = [...multiSelectCommon, ...ACCENT_VALIDATE] as const; -export const DrawerStyle = [getBackground()] as const; +export const DrawerStyle = [getBackground()] as const export const JsonEditorStyle = [LABEL] as const; @@ -928,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; @@ -968,6 +1021,10 @@ export type CarouselStyleType = StyleConfigType<typeof CarouselStyle>; export type RichTextEditorStyleType = StyleConfigType<typeof RichTextEditorStyle>; export type ResponsiveLayoutRowStyleType = StyleConfigType<typeof ResponsiveLayoutRowStyle>; export type ResponsiveLayoutColStyleType = StyleConfigType<typeof ResponsiveLayoutColStyle>; +export type NavLayoutStyleType = StyleConfigType<typeof NavLayoutStyle>; +export type NavLayoutItemStyleType = StyleConfigType<typeof NavLayoutItemStyle>; +export type NavLayoutItemHoverStyleType = StyleConfigType<typeof NavLayoutItemHoverStyle>; +export type NavLayoutItemActiveStyleType = StyleConfigType<typeof NavLayoutItemActiveStyle>; export function widthCalculator(margin: string) { const marginArr = margin?.trim().replace(/\s+/g,' ').split(" ") || ""; diff --git a/client/packages/lowcoder/src/comps/index.tsx b/client/packages/lowcoder/src/comps/index.tsx index 8aa9386b7..a14e4e682 100644 --- a/client/packages/lowcoder/src/comps/index.tsx +++ b/client/packages/lowcoder/src/comps/index.tsx @@ -142,6 +142,7 @@ import { ResponsiveLayoutComp } from "./comps/responsiveLayout"; import { VideoMeetingStreamComp } from "./comps/meetingComp/videoMeetingStreamComp"; import { ControlButton } from "./comps/meetingComp/controlButton"; import { VideoMeetingControllerComp } from "./comps/meetingComp/videoMeetingControllerComp"; +import { VideoSharingStreamComp } from "./comps/meetingComp/videoSharingStreamComp"; type Registry = { [key in UICompType]?: UICompManifest; @@ -560,7 +561,17 @@ const uiCompMap: Registry = { }, defaultDataFn: defaultContainerData, }, - + //ADDED BY FRED + sharingcomponent: { + name: trans("meeting.sharingCompName"), + enName: "Sharing", + description: trans("meeting.sharingCompName"), + categories: ["meeting"], + icon: VideoCompIcon, + keywords: trans("meeting.meetingCompKeywords"), + comp: VideoSharingStreamComp, + withoutLoading: true, + }, videocomponent: { name: trans("meeting.videoCompName"), enName: "Video", @@ -581,6 +592,7 @@ const uiCompMap: Registry = { comp: ControlButton, withoutLoading: true, }, + //END tabbedContainer: { name: trans("uiComp.tabbedContainerCompName"), enName: "Tabbed Container", @@ -931,7 +943,7 @@ const uiCompMap: Registry = { layoutInfo: { w: 13, h: 55, - } + }, }, mention: { name: trans("uiComp.mentionCompName"), diff --git a/client/packages/lowcoder/src/comps/uiCompRegistry.ts b/client/packages/lowcoder/src/comps/uiCompRegistry.ts index c48be999e..54d0f5d2c 100644 --- a/client/packages/lowcoder/src/comps/uiCompRegistry.ts +++ b/client/packages/lowcoder/src/comps/uiCompRegistry.ts @@ -58,6 +58,7 @@ export type UICompType = | "chart" | "meeting" | "videocomponent" + | "sharingcomponent" | "controlButton" | "imageEditor" | "calendar" diff --git a/client/packages/lowcoder/src/comps/utils/compDocUtil.ts b/client/packages/lowcoder/src/comps/utils/compDocUtil.ts index 0f82307f4..edb1bb42b 100644 --- a/client/packages/lowcoder/src/comps/utils/compDocUtil.ts +++ b/client/packages/lowcoder/src/comps/utils/compDocUtil.ts @@ -12,3 +12,14 @@ export function getComponentDocUrl(compType: UICompType) { return trans("docUrls.components", { compType }); } } +export function getComponentPlaygroundUrl(compType: UICompType) { + if (!compType) { + return ""; + } + switch (compType) { + case "module": + return trans("docUrls.module"); + default: + return trans("playground.url", { compType }); + } +} diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 40138fb17..73756da19 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -177,6 +177,7 @@ export const en = { "If the result is non-empty string, it is an error message. If empty or null, the validation passes. Example: ", manual: "Manual", map: "Mapped", + json: "JSON", use12Hours: "Use 12-hours", hourStep: "Hour step", minuteStep: "Minute step", @@ -318,6 +319,7 @@ export const en = { validate: "Validation message", border: "Border", borderRadius: "Border radius", + borderwidth: "Border width", background: "Background", headerBackground: "Header background", footerBackground: "Footer background", @@ -859,6 +861,7 @@ export const en = { audioCompDesc: "Audio component", audioCompKeywords: "", videoCompName: "Video", + sharingCompName: "Sharing", videoCompDesc: "Video component", videoCompKeywords: "", drawerCompName: "Drawer", @@ -905,6 +908,7 @@ export const en = { }, comp: { menuViewDocs: "View documentation", + menuViewPlayground: "View playground", menuUpgradeToLatest: "Upgrade to latest version", nameNotEmpty: "Can not be empty", nameRegex: @@ -1463,8 +1467,8 @@ export const en = { meetingName: "Meeting Name", localUserID: "Host User Id", userName: "Host User Name", - rtmToken : "Agora RTM Token", - rtcToken : "Agora RTC Token", + rtmToken: "Agora RTM Token", + rtcToken: "Agora RTC Token", videoCompText: "No video Text", profileImageUrl: "Profile Image Url", right: "Right", @@ -1481,18 +1485,21 @@ export const en = { actionBtnDesc: "Action Button", broadCast: "BroadCast Messages", title: "Meeting Title", - meetingCompName: "Meeting Controller", - videoCompName: "Video Stream", - videoSharingCompName: "Screen Sharing", - meetingControlCompName: "Controls Buttons", - meetingCompDesc: "Meeting component", - meetingCompControls: "Meeting control", - meetingCompKeywords: "", + //ADDED BY FRED + meetingCompName: "Agora Meeting Controller", + sharingCompName: "Screen share Stream", + videoCompName: "Camera Stream", + videoSharingCompName: "Screen share Stream", + meetingControlCompName: "Control Button", + meetingCompDesc: "Meeting Component", + meetingCompControls: "Meeting Control", + meetingCompKeywords: "Agora Meeting, Web Meeting, Collaboration", + //END iconSize: "Icon Size", userId: "userId", roomId: "roomId", - meetingActive : "Ongoing Meeting", - messages : "Broadcasted Messages", + meetingActive: "Ongoing Meeting", + messages: "Broadcasted Messages", }, settings: { title: "Settings", @@ -1841,6 +1848,8 @@ export const en = { preloadLibsEmpty: "No JavaScript libraries were added", preloadLibsAddBtn: "Add a library", saveSuccess: "Saved successfully", + AuthOrgTitle: "Workspace welcome Screen", + AuthOrgDescrition: "The URL for your users to Sign in to the current workspace.", }, branding: { title: "Branding", @@ -2272,13 +2281,13 @@ export const en = { }, docUrls: { docHome: "https://docs.lowcoder.cloud/", - components: "https://app.lowcoder.cloud/components?n={compType}", - module: "", + components: "https://app.lowcoder.cloud/components/{compType}", + module: "https://docs.lowcoder.cloud/lowcoder-documentation/build-applications/create-a-new-app/modules", optionList: "", - terms: "", - privacy: "", - aboutUs: "", - changeLog: "", + terms: "https://lowcoder.cloud/terms", + privacy: "https://lowcoder.cloud/privacy", + aboutUs: "https://lowcoder.cloud/about", + changeLog: "https://github.com/lowcoder-org/lowcoder/releases", introVideo: "", devNpmPlugin: "https://docs.lowcoder.cloud/lowcoder-extension/develop-data-source-plugins", @@ -2535,6 +2544,7 @@ export const en = { justify: "Justify both ends", }, playground: { + url: "https://app.lowcoder.cloud/playground/{compType}/1", data: "Data", preview: "Preview", property: "Properties", @@ -2710,4 +2720,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..6b97f4805 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -167,6 +167,7 @@ prop: { customRuleTooltip: "如果结果是非空字符串,则为错误消息.如果为空或null,则验证通过.\n示例:", manual: "手动", map: "映射", + json: "JSON", use12Hours: "使用12小时制", hourStep: "小时步长", minuteStep: "分钟步长", @@ -852,6 +853,7 @@ uiComp: { }, comp: { menuViewDocs: "查看文档", + menuViewPlayground: "查看组件游乐场", menuUpgradeToLatest: "升级到最新版本", nameNotEmpty: "不能为空", nameRegex: "必须以字母开头,只能包含字母、数字和下划线(_)", @@ -2106,7 +2108,7 @@ toggleButton: { }, docUrls: { docHome: "https://docs.lowcoder.cloud/", - components: "https://app.lowcoder.cloud/components?n={compType}", + components: "https://app.lowcoder.cloud/components/{compType}", module: "", optionList: "", terms: "", @@ -2391,6 +2393,7 @@ componentDoc: { justify: "两端对齐", }, playground: { + url: "https://app.lowcoder.cloud/playground/{compType}/1", data: "数据", preview: "预览", property: "属性", @@ -2557,6 +2560,15 @@ timeLine: { matchColumnsHeight: "匹配列高度", rowLayout: "行布局", columnsLayout: "栏目布局", + }, + navLayout: { + mode: "模式", + modeInline: "排队", + modeVertical: "垂直的", + width: "宽度", + widthTooltip: "数字或百分比,例如 520,60%", + navStyle: "菜单风格", + navItemStyle: "菜单项样式", } }; diff --git a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx index 8baf2944d..2820e14c9 100644 --- a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx @@ -88,6 +88,7 @@ export const CompStateIcon: { meeting: <LeftMeeting />, mermaid: <LeftChart />, videocomponent: <LeftMeeting />, + sharingcomponent: <LeftMeeting />, controlButton: <LeftButton />, tabbedContainer: <LeftContainer />, modal: <LeftModal />, diff --git a/client/packages/lowcoder/src/pages/setting/idSource/idSourceConstants.ts b/client/packages/lowcoder/src/pages/setting/idSource/idSourceConstants.ts index d4b09e173..d7e0dcbe2 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/idSourceConstants.ts +++ b/client/packages/lowcoder/src/pages/setting/idSource/idSourceConstants.ts @@ -48,7 +48,7 @@ export const authConfig = { sourceValue: AuthType.Ory, form: { ...clientIdandSecretConfig, - instanceId: "Instance ID", + baseUrl: "Base URL", scope: "Scope", }, }, @@ -57,7 +57,7 @@ export const authConfig = { sourceValue: AuthType.KeyCloak, form: { ...clientIdandSecretConfig, - instanceId: "Instance ID", + baseUrl: "Base URL", realm: "Realm", scope: "Scope", }, @@ -107,5 +107,7 @@ export type FormItemType = { authServerId?: string; publicKey?: ItemType; domain?: string; + baseUrl?: string; realm?: string; + scope?: string; }; diff --git a/client/packages/lowcoder/src/pages/setting/idSource/list.tsx b/client/packages/lowcoder/src/pages/setting/idSource/list.tsx index 62f121ac0..353fba1a4 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/list.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/list.tsx @@ -34,6 +34,7 @@ import { messageInstance, AddIcon } from "lowcoder-design"; import { currentOrgAdmin } from "../../../util/permissionUtils"; import CreateModal from "./createModal"; import _ from "lodash"; +import { HelpText } from "components/HelpText"; export const IdSourceList = (props: any) => { const user = useSelector(getUser); @@ -44,6 +45,18 @@ export const IdSourceList = (props: any) => { const [modalVisible, setModalVisible] = useState(false); const enableEnterpriseLogin = useSelector(selectSystemConfig)?.featureFlag?.enableEnterpriseLogin; + let protocol = window.location.protocol; + const port = window.location.port; + let currentDomain = window.location.hostname; + + // Show port only if it is not a standard port + if (port && port !== '80' && port !== '443') { + currentDomain += `:${port}`; + } + + const redirectUrl = encodeURIComponent(`${protocol}//${currentDomain}/apps`); + const loginUrl = `${protocol}//${currentDomain}/org/${currentOrgId}/auth/login?redirectUrl=${encodeURIComponent(redirectUrl)}`; + useEffect(() => { if (!currentOrgId) { return; @@ -154,6 +167,11 @@ export const IdSourceList = (props: any) => { )} /> </TableStyled> + + <div style={{ marginTop: 20, marginLeft: 12 }} className="section-title">{trans("advanced.AuthOrgTitle")}</div> + <HelpText style={{ marginBottom: 12, marginLeft: 12 }}>{trans("advanced.AuthOrgDescrition") + ": "}</HelpText> + <HelpText style={{ marginBottom: 12, marginLeft: 12 }}><a href={loginUrl} target="blank">{loginUrl}</a></HelpText> + </Level1SettingPageContentWithList> <CreateModal modalVisible={modalVisible} @@ -167,3 +185,4 @@ export const IdSourceList = (props: any) => { </> ); }; + 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 \ 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..33e718d58 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,7 +1,8 @@ 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 static org.lowcoder.sdk.auth.constants.Oauth2Constants.SCOPE_PLACEHOLDER; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -15,8 +16,9 @@ @Getter public class Oauth2KeycloakAuthConfig extends Oauth2SimpleAuthConfig { - protected String instanceId; + protected String baseUrl; protected String realm; + protected String scope; @JsonCreator public Oauth2KeycloakAuthConfig( @@ -27,13 +29,15 @@ 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("scope") String scope, @JsonProperty("authType") String authType) { super(id, enable, enableRegister, source, sourceName, clientId, clientSecret, authType); - this.instanceId = instanceId; + this.baseUrl = baseUrl; this.realm = realm; + this.scope = scope; } @@ -42,8 +46,9 @@ public Oauth2KeycloakAuthConfig( public String replaceAuthUrlClientIdPlaceholder(String url) { return super.replaceAuthUrlClientIdPlaceholder(url) - .replace(INSTANCE_ID_PLACEHOLDER, instanceId) - .replace(REALM_PLACEHOLDER, realm); + .replace(BASE_URL_PLACEHOLDER, baseUrl) + .replace(REALM_PLACEHOLDER, realm) + .replace(SCOPE_PLACEHOLDER, scope); } 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..d30715ca6 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,7 @@ 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.SCOPE_PLACEHOLDER; import javax.annotation.Nullable; @@ -14,7 +15,8 @@ @Getter public class Oauth2OryAuthConfig extends Oauth2SimpleAuthConfig { - protected String instanceId; + protected String baseUrl; + protected String scope; @JsonCreator public Oauth2OryAuthConfig( @@ -25,14 +27,18 @@ public Oauth2OryAuthConfig( String sourceName, String clientId, String clientSecret, - String instanceId, + String baseUrl, + String scope, String authType) { super(id, enable, enableRegister, source, sourceName, clientId, clientSecret, authType); - this.instanceId = instanceId; + this.baseUrl = baseUrl; + this.scope = scope; } @Override public String replaceAuthUrlClientIdPlaceholder(String url) { - return super.replaceAuthUrlClientIdPlaceholder(url).replace(INSTANCE_ID_PLACEHOLDER, instanceId); + return super.replaceAuthUrlClientIdPlaceholder(url) + .replace(BASE_URL_PLACEHOLDER, baseUrl) + .replace(SCOPE_PLACEHOLDER, scope); } } 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..38b7eea68 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,8 @@ 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"; + public static final String SCOPE_PLACEHOLDER = "$SCOPE"; // authorize url public static final String GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize" @@ -27,17 +28,17 @@ 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"; + + "&scope=" + SCOPE_PLACEHOLDER; - 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 + "&state=" + STATE_PLACEHOLDER - + "&scope=openid email profile"; + + "&scope=" + SCOPE_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"; } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/factory/AuthConfigFactoryImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/factory/AuthConfigFactoryImpl.java index 82a8b331a..e2f256f47 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/factory/AuthConfigFactoryImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/factory/AuthConfigFactoryImpl.java @@ -71,7 +71,8 @@ private Oauth2SimpleAuthConfig buildOauth2OryAuthConfig(AuthConfigRequest authCo org.lowcoder.sdk.constants.AuthSourceConstants.ORY_NAME, requireNonNull(authConfigRequest.getClientId(), "clientId can not be null."), authConfigRequest.getClientSecret(), - authConfigRequest.getInstanceId(), + authConfigRequest.getString("baseUrl"), + authConfigRequest.getString("scope"), authConfigRequest.getAuthType()); } @@ -84,8 +85,9 @@ private Oauth2SimpleAuthConfig buildOauth2KeycloakAuthConfig(AuthConfigRequest a org.lowcoder.sdk.constants.AuthSourceConstants.KEYCLOAK_NAME, requireNonNull(authConfigRequest.getClientId(), "clientId can not be null."), authConfigRequest.getClientSecret(), - authConfigRequest.getInstanceId(), + authConfigRequest.getString("baseUrl"), authConfigRequest.getString("realm"), + authConfigRequest.getString("scope"), authConfigRequest.getAuthType()); }