diff --git a/client/VERSION b/client/VERSION index b38513792..fad066f80 100644 --- a/client/VERSION +++ b/client/VERSION @@ -1 +1 @@ -2.4.12 \ No newline at end of file +2.5.0 \ No newline at end of file diff --git a/client/package.json b/client/package.json index daecce28c..5abacbd78 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-frontend", - "version": "2.4.12", + "version": "2.5.0", "type": "module", "private": true, "workspaces": [ diff --git a/client/packages/lowcoder-cli-template-typescript/package.json b/client/packages/lowcoder-cli-template-typescript/package.json index f4288bacf..80edb46ec 100644 --- a/client/packages/lowcoder-cli-template-typescript/package.json +++ b/client/packages/lowcoder-cli-template-typescript/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-cli-template-typescript", - "version": "0.0.20", + "version": "0.0.22", "type": "module", "scripts": { "start": "NODE_OPTIONS=--max_old_space_size=6144 vite", @@ -22,7 +22,9 @@ } }, "dependencies": { + "@observablehq/inspector": "^5.0.1", "@observablehq/runtime": "^4.8.2", + "@observablehq/stdlib": "^5.8.8", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", "lowcoder-cli": "^0.0.30", diff --git a/client/packages/lowcoder-cli-template-typescript/src/vendors/Chart.jsx b/client/packages/lowcoder-cli-template-typescript/src/vendors/Chart.jsx index befadc8f7..a3e37f56d 100644 --- a/client/packages/lowcoder-cli-template-typescript/src/vendors/Chart.jsx +++ b/client/packages/lowcoder-cli-template-typescript/src/vendors/Chart.jsx @@ -1,6 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types' -import { Runtime, Inspector } from '@observablehq/runtime'; +import { Runtime } from '@observablehq/runtime'; +import { Inspector } from "@observablehq/inspector"; +import { Library } from "@observablehq/stdlib"; + +const library = new Library(); function Chart(props) { const [chartRef, setChartRef] = React.useState(); @@ -16,21 +20,20 @@ function Chart(props) { main.variable().define('translateXtoY', function() { return x => 50 * Math.sin((Math.PI / 50) * x - (1 / 2) * Math.PI) + 50; }); - main.variable().define('d3', ['require'], function(require) { - return require('https://d3js.org/d3.v5.min.js'); + main.variable().define('d3', [], function() { + return Library.require('https://d3js.org/d3.v5.min.js'); }); // Define the HillChart class - main.variable().define('HillChart', ['d3', 'DOM', 'translateXtoY'], function(d3, DOM, translateXtoY) { + main.variable().define('HillChart', ['d3', 'translateXtoY'], function(d3, translateXtoY) { return class HillChart { constructor(chart_height, chart_width, items) { this.chart_height = chart_height; this.chart_width = chart_width; this.items = items; - - this.svg = d3.select(DOM.svg(this.chart_width, this.chart_height)).attr('viewBox', `-20 -20 ${this.chart_width + 80} ${this.chart_height + 20}`); + + this.svg = d3.select(library.DOM.svg(this.chart_width, this.chart_height)).attr('viewBox', `-20 -20 ${this.chart_width + 80} ${this.chart_height + 20}`); } - render() { const xScale = d3 diff --git a/client/packages/lowcoder-cli/package.json b/client/packages/lowcoder-cli/package.json index 7dbb8e48d..f9ce7c3aa 100644 --- a/client/packages/lowcoder-cli/package.json +++ b/client/packages/lowcoder-cli/package.json @@ -38,7 +38,6 @@ "vite-plugin-svgr": "^2.2.2" }, "devDependencies": { - "@types/axios": "^0.14.0", "typescript": "^4.8.4" }, "peerDependencies": { diff --git a/client/packages/lowcoder-design/package.json b/client/packages/lowcoder-design/package.json index fb684d4e9..40c8c1024 100644 --- a/client/packages/lowcoder-design/package.json +++ b/client/packages/lowcoder-design/package.json @@ -9,10 +9,10 @@ "dependencies": { "colord": "^2.9.3", "react-fontawesome": "^0.2.0", - "react-markdown": "^8.0.0", + "react-markdown": "^9.0.1", "react-virtualized": "^9.22.3", - "rehype-raw": "^6.1.1", - "rehype-sanitize": "^5.0.1", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.0", "simplebar": "^6.2.5", "simplebar-react": "^3.2.4" diff --git a/client/packages/lowcoder-design/src/components/markdown.tsx b/client/packages/lowcoder-design/src/components/markdown.tsx index 45eefd536..1260f007f 100644 --- a/client/packages/lowcoder-design/src/components/markdown.tsx +++ b/client/packages/lowcoder-design/src/components/markdown.tsx @@ -4,7 +4,7 @@ import { lazy } from "react"; import rehypeRaw from "rehype-raw"; import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; import remarkGfm from "remark-gfm"; -import type { ReactMarkdownOptions } from "react-markdown/lib/react-markdown"; +import type { Options as ReactMarkdownOptions } from "react-markdown/lib"; const ReactMarkdown = lazy(() => import('react-markdown')); diff --git a/client/packages/lowcoder-design/src/components/tacoInput.tsx b/client/packages/lowcoder-design/src/components/tacoInput.tsx index 125840011..e7ce12111 100644 --- a/client/packages/lowcoder-design/src/components/tacoInput.tsx +++ b/client/packages/lowcoder-design/src/components/tacoInput.tsx @@ -331,12 +331,14 @@ const FormInput = (props: { check: (value: string) => boolean; }; formName?: string; + onBlur?: () => void; onChange?: (value: string, valid: boolean) => void; className?: string; inputRef?: Ref; msg?: string; + defaultValue?: string; }) => { - const { mustFill, checkRule, label, placeholder, onChange, formName, className, inputRef } = + const { mustFill, checkRule, label, placeholder, onBlur, onChange, formName, className, inputRef, defaultValue } = props; const [valueValid, setValueValid] = useState(true); return ( @@ -350,6 +352,7 @@ const FormInput = (props: { ref={inputRef} name={formName} placeholder={placeholder} + defaultValue={defaultValue} onChange={(e) => { let valid = true; if (checkRule) { @@ -358,6 +361,7 @@ const FormInput = (props: { } onChange && onChange(e.target.value, valid); }} + onBlur={() => onBlur?.()} /> ); diff --git a/client/packages/lowcoder-sdk/package.json b/client/packages/lowcoder-sdk/package.json index 62b9576a9..ed81c3ba7 100644 --- a/client/packages/lowcoder-sdk/package.json +++ b/client/packages/lowcoder-sdk/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-sdk", - "version": "2.4.14", + "version": "2.4.16", "type": "module", "files": [ "src", diff --git a/client/packages/lowcoder/index.html b/client/packages/lowcoder/index.html index 2c3d6cd34..f3019a0cd 100644 --- a/client/packages/lowcoder/index.html +++ b/client/packages/lowcoder/index.html @@ -51,6 +51,10 @@ +
diff --git a/client/packages/lowcoder/package.json b/client/packages/lowcoder/package.json index d5ef8d284..d520a927d 100644 --- a/client/packages/lowcoder/package.json +++ b/client/packages/lowcoder/package.json @@ -38,7 +38,7 @@ "@types/react-virtualized": "^9.21.21", "animate.css": "^4.1.1", "antd": "^5.20.0", - "axios": "^1.7.4", + "axios": "^1.7.7", "buffer": "^6.0.3", "clsx": "^2.0.0", "cnchar": "^3.2.4", @@ -86,6 +86,7 @@ "react-sortable-hoc": "^2.0.0", "react-test-renderer": "^18.1.0", "react-use": "^17.3.2", + "react-webcam": "^7.2.0", "really-relaxed-json": "^0.3.2", "redux-devtools-extension": "^2.13.9", "redux-saga": "^1.1.3", diff --git a/client/packages/lowcoder/src/api/apiResponses.ts b/client/packages/lowcoder/src/api/apiResponses.ts index c985bcb9d..5999bfcca 100644 --- a/client/packages/lowcoder/src/api/apiResponses.ts +++ b/client/packages/lowcoder/src/api/apiResponses.ts @@ -12,6 +12,13 @@ export interface GenericApiResponse { data: T; } +export interface FetchGroupApiResponse extends GenericApiResponse { + totalAdmins: number, + totalAdminsAndDevelopers: number, + totalDevelopersOnly: number, + totalOtherMembers: number, +} + // NO_DATASOURCES_FOUND, 1000, "Unable to find {0} with id {1}" // INVALID_PARAMTER, 4000, "Invalid parameter {0} provided in the input" // PLUGIN_NOT_INSTALLED, 4001, "Plugin {0} not installed" diff --git a/client/packages/lowcoder/src/api/applicationApi.ts b/client/packages/lowcoder/src/api/applicationApi.ts index d38ee1843..a0edb7424 100644 --- a/client/packages/lowcoder/src/api/applicationApi.ts +++ b/client/packages/lowcoder/src/api/applicationApi.ts @@ -98,6 +98,7 @@ class ApplicationApi extends Api { static publicToMarketplaceURL = (applicationId: string) => `/applications/${applicationId}/public-to-marketplace`; static getMarketplaceAppURL = (applicationId: string) => `/applications/${applicationId}/view_marketplace`; static setAppEditingStateURL = (applicationId: string) => `/applications/editState/${applicationId}`; + static serverSettingsURL = () => `/serverSettings`; static fetchHomeData(request: HomeDataPayload): AxiosPromise { return Api.get(ApplicationApi.fetchHomeDataURL, request); @@ -240,6 +241,10 @@ class ApplicationApi extends Api { editingFinished, }); } + + static fetchServerSettings(): AxiosPromise { + return Api.get(ApplicationApi.serverSettingsURL()); + } } export default ApplicationApi; diff --git a/client/packages/lowcoder/src/api/configApi.ts b/client/packages/lowcoder/src/api/configApi.ts index 6af3161fc..fc756069e 100644 --- a/client/packages/lowcoder/src/api/configApi.ts +++ b/client/packages/lowcoder/src/api/configApi.ts @@ -17,6 +17,10 @@ class ConfigApi extends Api { } return Api.get(authConfigURL); } + + static fetchDeploymentId(): AxiosPromise { + return Api.get(`${ConfigApi.configURL}/deploymentId`); + } } export default ConfigApi; diff --git a/client/packages/lowcoder/src/api/idSourceApi.ts b/client/packages/lowcoder/src/api/idSourceApi.ts index 98e6141d7..00f2b7fcf 100644 --- a/client/packages/lowcoder/src/api/idSourceApi.ts +++ b/client/packages/lowcoder/src/api/idSourceApi.ts @@ -44,8 +44,8 @@ class IdSourceApi extends Api { return Api.post(IdSourceApi.saveConfigURL, request); } - static deleteConfig(id: string): AxiosPromise { - return Api.delete(IdSourceApi.deleteConfigURL(id)); + static deleteConfig(id: string, deleteConfig?: boolean): AxiosPromise { + return Api.delete(IdSourceApi.deleteConfigURL(id), {delete: deleteConfig}); } static syncManual(authType: string): AxiosPromise { diff --git a/client/packages/lowcoder/src/api/orgApi.ts b/client/packages/lowcoder/src/api/orgApi.ts index c3d66e557..6e7c532e4 100644 --- a/client/packages/lowcoder/src/api/orgApi.ts +++ b/client/packages/lowcoder/src/api/orgApi.ts @@ -52,6 +52,7 @@ export class OrgApi extends Api { static deleteOrgURL = (orgId: string) => `/organizations/${orgId}`; static updateOrgURL = (orgId: string) => `/organizations/${orgId}/update`; static fetchUsage = (orgId: string) => `/organizations/${orgId}/api-usage`; + static fetchOrgsByEmailURL = (email: string) => `organizations/byuser/${email}`; static createGroup(request: { name: string }): AxiosPromise> { return Api.post(OrgApi.createGroupURL, request); @@ -141,6 +142,9 @@ export class OrgApi extends Api { return Api.get(OrgApi.fetchUsage(orgId), { lastMonthOnly: true }); } + static fetchOrgsByEmail(email: string): AxiosPromise { + return Api.get(OrgApi.fetchOrgsByEmailURL(email)); + } } export default OrgApi; diff --git a/client/packages/lowcoder/src/api/subscriptionApi.ts b/client/packages/lowcoder/src/api/subscriptionApi.ts index c8859e6da..02a3ff5f6 100644 --- a/client/packages/lowcoder/src/api/subscriptionApi.ts +++ b/client/packages/lowcoder/src/api/subscriptionApi.ts @@ -1,121 +1,16 @@ import Api from "api/api"; -import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; -import { useSelector } from "react-redux"; -import { getUser, getCurrentUser } from "redux/selectors/usersSelectors"; -import { useEffect, useState } from "react"; +import axios, { AxiosInstance, AxiosRequestConfig, CancelToken } from "axios"; +import { useDispatch, useSelector } from "react-redux"; +import { useEffect, useState} from "react"; import { calculateFlowCode } from "./apiUtils"; - -// Interfaces -export interface CustomerAddress { - line1: string; - line2: string; - city: string; - state: string; - country: string; - postalCode: string; -} - -export interface LowcoderNewCustomer { - hostname: string; - email: string; - orgId: string; - userId: string; - userName: string; - type: string; - companyName: string; - address?: CustomerAddress; -} - -export interface LowcoderSearchCustomer { - hostname: string; - email: string; - orgId: string; - userId: string; -} - -interface LowcoderMetadata { - lowcoder_host: string; - lowcoder_orgId: string; - lowcoder_type: string; - lowcoder_userId: string; -} - -export interface StripeCustomer { - id: string; - object: string; - address?: object | null; - balance: number; - created: number; - currency: string | null; - default_source: string | null; - delinquent: boolean; - description: string | null; - discount: string | null; - email: string; - invoice_prefix: string; - invoice_settings: object | null; - livemode: boolean; - metadata: LowcoderMetadata; - name: string; - phone: string | null; - preferred_locales: string[]; - shipping: string | null; - tax_exempt: string; - test_clock: string | null; -} - -export interface Pricing { - type: string; - amount: string; -} - -export interface Product { - title?: string; - description?: string; - image?: string; - pricingType: string; - product: string; - activeSubscription: boolean; - accessLink: string; - subscriptionId: string; - checkoutLink: string; - checkoutLinkDataLoaded?: boolean; - type?: string; - quantity_entity?: string; -} - -export interface SubscriptionItem { - id: string; - object: string; - plan: { - id: string; - product: string; - }; - quantity: number; -} - -export interface Subscription { - id: string; - collection_method: string; - current_period_end: number; - current_period_start: number; - product: string; - currency: string; - interval: string; - tiers_mode: string; - status: string; - start_date: number; - quantity: number; - billing_scheme: string; - price: string; -} - -export interface SubscriptionsData { - subscriptions: Subscription[]; - subscriptionDataLoaded: boolean; - subscriptionDataError: boolean; - loading: boolean; -} +import { fetchGroupsAction, fetchOrgUsersAction } from "redux/reduxActions/orgActions"; +import { getOrgUsers } from "redux/selectors/orgSelectors"; +import { AppState } from "@lowcoder-ee/redux/reducers"; +import type { + LowcoderNewCustomer, + LowcoderSearchCustomer, + StripeCustomer, +} from "@lowcoder-ee/constants/subscriptionConstants"; export type ResponseType = { response: any; @@ -148,17 +43,54 @@ const getAxiosInstance = (clientSecret?: string) => { }; class SubscriptionApi extends Api { - static async secureRequest(body: any): Promise { + static async secureRequest(body: any, timeout: number = 6000): Promise { let response; + const axiosInstance = getAxiosInstance(); + + // Create a cancel token and set timeout for cancellation + const source = axios.CancelToken.source(); + const timeoutId = setTimeout(() => { + source.cancel("Request timed out."); + }, timeout); + + // Request configuration with cancel token + const requestConfig: AxiosRequestConfig = { + method: "POST", + withCredentials: true, + data: body, + cancelToken: source.token, // Add cancel token + }; + try { - response = await getAxiosInstance().request({ - method: "POST", - withCredentials: true, - data: body, - }); + response = await axiosInstance.request(requestConfig); } catch (error) { - console.error("Error at Secure Flow Request:", error); + if (axios.isCancel(error)) { + // Retry once after timeout cancellation + try { + // Reset the cancel token and retry + const retrySource = axios.CancelToken.source(); + const retryTimeoutId = setTimeout(() => { + retrySource.cancel("Retry request timed out."); + }, 10000); + + response = await axiosInstance.request({ + ...requestConfig, + cancelToken: retrySource.token, + }); + + clearTimeout(retryTimeoutId); + } catch (retryError) { + console.warn("Error at Secure Flow Request. Retry failed:", retryError); + throw retryError; + } + } else { + console.warn("Error at Secure Flow Request:", error); + throw error; + } + } finally { + clearTimeout(timeoutId); // Clear the initial timeout } + return response; } } @@ -204,15 +136,30 @@ export const searchCustomersSubscriptions = async (Customer: LowcoderSearchCusto method: "post", headers: lcHeaders }; + try { const result = await SubscriptionApi.secureRequest(apiBody); - if (result?.data?.data?.length > 0) { - return result?.data?.data; - } - else if (result.data.success == "false" && result.data.reason == "customerNotFound") { + if (!result || !result.data) { return []; } + + // Filter out entries with `"success": "false"` + const validEntries = result.data.filter((entry: any) => entry.success !== "false"); + + // Flatten the data arrays and filter out duplicates by `id` + const uniqueSubscriptions = Object.values( + validEntries.reduce((acc: Record, entry: any) => { + entry.data.forEach((subscription: any) => { + if (!acc[subscription.id]) { + acc[subscription.id] = subscription; + } + }); + return acc; + }, {}) + ); + + return uniqueSubscriptions; } catch (error) { console.error("Error searching customer:", error); throw error; @@ -227,7 +174,7 @@ export const createCustomer = async (subscriptionCustomer: LowcoderNewCustomer) headers: lcHeaders }; try { - const result = await SubscriptionApi.secureRequest(apiBody); + const result = await SubscriptionApi.secureRequest(apiBody, 15000); return result?.data as StripeCustomer; } catch (error) { console.error("Error creating customer:", error); @@ -260,7 +207,7 @@ export const getProducts = async () => { }; try { const result = await SubscriptionApi.secureRequest(apiBody); - return result?.data as any; + return result?.data?.data as any[]; } catch (error) { console.error("Error fetching product:", error); throw error; @@ -291,226 +238,55 @@ export const createCheckoutLink = async (customer: StripeCustomer, priceId: stri } }; -// Hooks - -export const InitializeSubscription = () => { - const [customer, setCustomer] = useState(null); - const [isCreatingCustomer, setIsCreatingCustomer] = useState(false); // Track customer creation - const [customerDataError, setCustomerDataError] = useState(false); - const [subscriptions, setSubscriptions] = useState([]); - const [subscriptionDataLoaded, setSubscriptionDataLoaded] = useState(false); - const [subscriptionDataError, setSubscriptionDataError] = useState(false); - const [checkoutLinkDataLoaded, setCheckoutLinkDataLoaded] = useState(false); - const [checkoutLinkDataError, setCheckoutLinkDataError] = useState(false); - const [products, setProducts] = useState([ - { - pricingType: "Monthly, per User", - activeSubscription: false, - accessLink: "1PhH38DDlQgecLSfSukEgIeV", - product: "QW8L3WPMiNjQjI", - subscriptionId: "", - checkoutLink: "", - checkoutLinkDataLoaded: false, - type: "org", - quantity_entity: "orgUser", - }, - { - pricingType: "Monthly, per User", - activeSubscription: false, - accessLink: "1Pf65wDDlQgecLSf6OFlbsD5", - product: "QW8MpIBHxieKXd", - checkoutLink: "", - checkoutLinkDataLoaded: false, - subscriptionId: "", - type: "user", - quantity_entity: "singleItem", - }, - { - pricingType: "Monthly, per User", - activeSubscription: false, - accessLink: "1PttHIDDlQgecLSf0XP27tXt", - product: "QlQ7cdOh8Lv4dy", - subscriptionId: "", - checkoutLink: "", - checkoutLinkDataLoaded: false, - type: "org", - quantity_entity: "singleItem", - }, - ]); - - const user = useSelector(getUser); - const currentUser = useSelector(getCurrentUser); - const currentOrg = user.orgs.find(org => org.id === user.currentOrgId); - const orgID = user.currentOrgId; - const domain = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : ''); - const admin = user.orgRoleMap.get(orgID) === "admin" ? "admin" : "member"; - - const subscriptionSearchCustomer: LowcoderSearchCustomer = { - hostname: domain, - email: currentUser.email, - orgId: orgID, - userId: user.id, - }; - - const subscriptionNewCustomer: LowcoderNewCustomer = { - hostname: domain, - email: currentUser.email, - orgId: orgID, - userId: user.id, - userName: user.username, - type: admin, - companyName: currentOrg?.name || "Unknown", - }; - - useEffect(() => { - const initializeCustomer = async () => { - try { - setIsCreatingCustomer(true); - const existingCustomer = await searchCustomer(subscriptionSearchCustomer); - if (existingCustomer != null) { - setCustomer(existingCustomer); - } else { - const newCustomer = await createCustomer(subscriptionNewCustomer); - setCustomer(newCustomer); - } - } catch (error) { - setCustomerDataError(true); - } finally { - setIsCreatingCustomer(false); - } - }; - - initializeCustomer(); - }, []); - - useEffect(() => { - const fetchSubscriptions = async () => { - if (customer) { - try { - const subs = await searchSubscriptions(customer.id); - setSubscriptions(subs); - setSubscriptionDataLoaded(true); - } catch (error) { - setSubscriptionDataError(true); - } - } - }; - - fetchSubscriptions(); - }, [customer]); - - useEffect(() => { - const prepareCheckout = async () => { - if (subscriptionDataLoaded) { - try { - const updatedProducts = await Promise.all( - products.map(async (product) => { - const matchingSubscription = subscriptions.find( - (sub) => sub.plan.id === "price_" + product.accessLink - ); - - if (matchingSubscription) { - return { - ...product, - activeSubscription: true, - checkoutLinkDataLoaded: true, - subscriptionId: matchingSubscription.id.substring(4), - }; - } else { - const checkoutLink = await createCheckoutLink(customer!, product.accessLink, 1); - return { - ...product, - activeSubscription: false, - checkoutLink: checkoutLink ? checkoutLink.url : "", - checkoutLinkDataLoaded: true, - }; - } - }) - ); - - setProducts(updatedProducts); - } catch (error) { - setCheckoutLinkDataError(true); - } - } - }; - - prepareCheckout(); - }, [subscriptionDataLoaded]); - - return { - customer, - isCreatingCustomer, - customerDataError, - subscriptions, - subscriptionDataLoaded, - subscriptionDataError, - checkoutLinkDataLoaded, - checkoutLinkDataError, - products, - admin, +// Function to get subscription details from Stripe +export const getSubscriptionDetails = async (subscriptionId: string) => { + const apiBody = { + path: "webhook/secure/get-subscription-details", + method: "post", + data: { "subscriptionId": subscriptionId }, + headers: lcHeaders, }; + try { + const result = await SubscriptionApi.secureRequest(apiBody); + return result?.data; + } catch (error) { + console.error("Error fetching subscription details:", error); + throw error; + } }; -export enum SubscriptionProducts { - SUPPORT = "QW8L3WPMiNjQjI", - MEDIAPACKAGE = 'QW8MpIBHxieKXd', - AZUREAPIS = 'premium', - GOOGLEAPIS = 'enterprise', - AWSAPIS = 'enterprise-global', - PRIVATECLOUD = 'private-cloud', - MATRIXCLOUD = 'matrix-cloud', - AGORATOKENSERVER = 'agora-tokenserver', - SIGNALSERVER = 'signal-server', - DATABASE = 'database', - STORAGE = 'storage', - IOSAPP = 'ios-app', - ANDROIDAPP = 'android-app', - AUDITLOG = 'audit-log', - APPLOG = 'app-log', - ENVIRONMENTS = 'environments', - GITREPOS = 'git-repos', -} - -export const CheckSubscriptions = () => { - const [subscriptions, setSubscriptions] = useState([]); - const [subscriptionDataLoaded, setSubscriptionDataLoaded] = useState(false); - const [subscriptionDataError, setSubscriptionDataError] = useState(false); - const [loading, setLoading] = useState(true); - - const user = useSelector(getUser); - const currentUser = useSelector(getCurrentUser); - const orgID = user.currentOrgId; - const domain = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : ''); - - const subscriptionSearchCustomer: LowcoderSearchCustomer = { - hostname: domain, - email: currentUser.email, - orgId: orgID, - userId: user.id, +// Function to get invoice documents from Stripe +export const getInvoices = async (subscriptionId: string) => { + const apiBody = { + path: "webhook/secure/get-subscription-invoices", + method: "post", + data: { "subscriptionId": subscriptionId }, + headers: lcHeaders, }; + try { + const result = await SubscriptionApi.secureRequest(apiBody); + return result?.data?.data ?? []; + } catch (error) { + console.error("Error fetching invoices:", error); + throw error; + } +}; - useEffect(() => { - const fetchCustomerAndSubscriptions = async () => { - try { - const subs = await searchCustomersSubscriptions(subscriptionSearchCustomer); - setSubscriptions(subs); - setSubscriptionDataLoaded(true); - } catch (error) { - setSubscriptionDataError(true); - } finally { - setLoading(false); - } - }; - fetchCustomerAndSubscriptions(); - }, [subscriptionSearchCustomer]); - - return { - subscriptions, - subscriptionDataLoaded, - subscriptionDataError, - loading, +// Function to get a customer Portal Session from Stripe +export const getCustomerPortalSession = async (customerId: string) => { + const apiBody = { + path: "webhook/secure/create-customer-portal-session", + method: "post", + data: { "customerId": customerId }, + headers: lcHeaders, }; + try { + const result = await SubscriptionApi.secureRequest(apiBody); + return result?.data; + } catch (error) { + console.error("Error fetching invoices:", error); + throw error; + } }; export default SubscriptionApi; diff --git a/client/packages/lowcoder/src/api/supportApi.ts b/client/packages/lowcoder/src/api/supportApi.ts index 134a9c93f..ab795ce28 100644 --- a/client/packages/lowcoder/src/api/supportApi.ts +++ b/client/packages/lowcoder/src/api/supportApi.ts @@ -1,5 +1,5 @@ import Api from "api/api"; -import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; +import axios, { AxiosInstance, AxiosRequestConfig, CancelToken } from "axios"; import { calculateFlowCode } from "./apiUtils"; export type ResponseType = { @@ -64,15 +64,53 @@ class SupportApi extends Api { static async secureRequest(body: any): Promise { let response; + const axiosInstance = getAxiosInstance(); + + // Create a cancel token and set timeout for cancellation + const source = axios.CancelToken.source(); + const timeoutId = setTimeout(() => { + source.cancel("Request timed out."); + }, 10000); + + // Request configuration with cancel token + const requestConfig: AxiosRequestConfig = { + method: "POST", + withCredentials: true, + data: body, + cancelToken: source.token, // Add cancel token + }; + try { - response = await getAxiosInstance().request({ - method: "POST", - withCredentials: true, - data: body, - }); + response = await axiosInstance.request(requestConfig); } catch (error) { - console.error("Error at Support Flow Request:", error); + if (axios.isCancel(error)) { + console.warn("Request cancelled due to timeout:", error.message); + // Retry once after timeout cancellation + try { + // Reset the cancel token and retry + const retrySource = axios.CancelToken.source(); + const retryTimeoutId = setTimeout(() => { + retrySource.cancel("Retry request timed out."); + }, 15000); + + response = await axiosInstance.request({ + ...requestConfig, + cancelToken: retrySource.token, + }); + + clearTimeout(retryTimeoutId); + } catch (retryError) { + console.error("Retry failed:", retryError); + throw retryError; + } + } else { + console.error("Error at Support Flow Request:", error); + throw error; + } + } finally { + clearTimeout(timeoutId); // Clear the initial timeout } + return response; } @@ -80,17 +118,28 @@ class SupportApi extends Api { // API Functions -export const searchCustomerTickets = async (orgID : string, currentUserId : string, domain : string) => { +export const searchCustomerTickets = async ( + deploymentId : string, + orgID : string, + currentUserId : string +) => { const apiBody = { path: "webhook/support/get-issues", - data: {"host" : domain, "orgId" : orgID, "userId" : currentUserId}, + data: {"hostId" : deploymentId, "orgId" : orgID, "userId" : currentUserId}, method: "post", headers: lcHeaders }; try { const result = await SupportApi.secureRequest(apiBody); - return result.data as TicketList; + + if (!result || !result.data) { + return []; + } + + const validEntries = result.data.filter((entry: any) => entry.success !== "false"); + + return validEntries as TicketList; } catch (error) { console.error("Error searching Support Tickets: ", error); throw error; @@ -114,11 +163,20 @@ export const getTicket = async (ticketKey : string) => { } }; -export const createTicket = async (orgID : string, currentUserId : string, subscriptionId : string, domain : string, summary: string, description : string, errors : string) => { +export const createTicket = async ( + domain: string, + deploymentId : string, + orgID : string, + orgName : string, + currentUserId : string, + subscriptionId : string, + summary: string, + description : string, + errors : string) => { const apiBody = { path: "webhook/support/create-ticket", - data: {"host" : domain, "orgId" : orgID, "userId" : currentUserId, "subscriptionId": subscriptionId, "summary" : summary, "description" : description, "errors" : errors}, + data: {"domain" : domain, "hostId" : deploymentId, "orgId" : orgID, "orgName" : orgName, "userId" : currentUserId, "subscriptionId": subscriptionId, "summary" : summary, "description" : description, "errors" : errors}, method: "post", headers: lcHeaders }; diff --git a/client/packages/lowcoder/src/app.tsx b/client/packages/lowcoder/src/app.tsx index f6cbdac58..e0be5b5ce 100644 --- a/client/packages/lowcoder/src/app.tsx +++ b/client/packages/lowcoder/src/app.tsx @@ -28,6 +28,7 @@ import { ADMIN_APP_URL, ORG_AUTH_FORGOT_PASSWORD_URL, ORG_AUTH_RESET_PASSWORD_URL, + ADMIN_AUTH_URL, } from "constants/routesURL"; import React from "react"; import { createRoot } from "react-dom/client"; @@ -55,7 +56,7 @@ import { getBrandingConfig } from "./redux/selectors/configSelectors"; import { buildMaterialPreviewURL } from "./util/materialUtils"; import GlobalInstances from 'components/GlobalInstances'; // import posthog from 'posthog-js' -import { fetchHomeData } from "./redux/reduxActions/applicationActions"; +import { fetchHomeData, fetchServerSettingsAction } from "./redux/reduxActions/applicationActions"; import { getNpmPackageMeta } from "./comps/utils/remote"; import { packageMetaReadyAction, setLowcoderCompsLoading } from "./redux/reduxActions/npmPluginActions"; @@ -94,6 +95,7 @@ type AppIndexProps = { fetchHomeData: (currentUserAnonymous?: boolean | undefined) => void; fetchLowcoderCompVersions: () => void; getCurrentUser: () => void; + fetchServerSettings: () => void; favicon: string; brandName: string; uiLanguage: string; @@ -102,6 +104,7 @@ type AppIndexProps = { class AppIndex extends React.Component { componentDidMount() { this.props.getCurrentUser(); + this.props.fetchServerSettings(); // if (!this.props.currentUserAnonymous) { // this.props.fetchHomeData(this.props.currentUserAnonymous); // } @@ -238,7 +241,6 @@ class AppIndex extends React.Component { rel="apple-touch-startup-image" href="https://raw.githubusercontent.com/lowcoder-org/lowcoder-media-assets/main/images/Lowcoder%20Logo%20512.png" /> - { rel="stylesheet" />, // adding Clearbit Support for Analytics - , ]} @@ -337,6 +333,7 @@ class AppIndex extends React.Component { // component={ApplicationListPage} component={LazyApplicationHome} /> + ({ dispatch(setLowcoderCompsLoading(false)); } }, + fetchServerSettings: () => { + dispatch(fetchServerSettingsAction()); + } }); const AppIndexWithProps = connect(mapStateToProps, mapDispatchToProps)(AppIndex); diff --git a/client/packages/lowcoder/src/appView/AppViewInstance.tsx b/client/packages/lowcoder/src/appView/AppViewInstance.tsx index 9cdd54760..9c01c7772 100644 --- a/client/packages/lowcoder/src/appView/AppViewInstance.tsx +++ b/client/packages/lowcoder/src/appView/AppViewInstance.tsx @@ -105,17 +105,17 @@ export class AppViewInstance { }); await DatasourceApi.fetchJsDatasourceByApp(this.appId).then((res) => { - res.data.data.forEach((i) => { + res.data?.data?.forEach((i) => { registryDataSourcePlugin(i.type, i.id, i.pluginDefinition); }); }); setGlobalSettings({ - orgCommonSettings: data.data.orgCommonSettings, + orgCommonSettings: data?.data?.orgCommonSettings, }); - finalAppDsl = data.data.applicationDSL; - finalModuleDslMap = data.data.moduleDSL; + finalAppDsl = data?.data?.applicationDSL || {}; + finalModuleDslMap = data?.data?.moduleDSL || {}; } if (this.options.moduleInputs && this.isModuleDSL(finalAppDsl)) { diff --git a/client/packages/lowcoder/src/base/codeEditor/codeEditor.tsx b/client/packages/lowcoder/src/base/codeEditor/codeEditor.tsx index e9bc3cc33..02630eefe 100644 --- a/client/packages/lowcoder/src/base/codeEditor/codeEditor.tsx +++ b/client/packages/lowcoder/src/base/codeEditor/codeEditor.tsx @@ -220,11 +220,13 @@ function useCodeMirror( const showLineNum = props.showLineNum ?? getStyle(props.styleName).showLineNum; const handleChange = useCallback( - debounce((state: EditorState) => { + (state: EditorState) => { window.clearTimeout(isTypingRef.current); - isTypingRef.current = window.setTimeout(() => (isTypingRef.current = 0), 100); - onChange?.(state); - }, 1000) + isTypingRef.current = window.setTimeout(() => { + isTypingRef.current = 0; + onChange?.(state); + }, 500); + } , [onChange] ); diff --git a/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx b/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx index c81aa9542..50f13ffdc 100644 --- a/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx +++ b/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx @@ -576,8 +576,8 @@ export default function ThemeSettingsSelector(props: ColorConfigProps) { setGridRowHeight(value.toString())} onChangeComplete={(value) => gridSizeInputBlur(value.toString())} diff --git a/client/packages/lowcoder/src/components/layout/SideBarSection.tsx b/client/packages/lowcoder/src/components/layout/SideBarSection.tsx index c42028f9d..e268b32a9 100644 --- a/client/packages/lowcoder/src/components/layout/SideBarSection.tsx +++ b/client/packages/lowcoder/src/components/layout/SideBarSection.tsx @@ -24,9 +24,9 @@ export const SideBarSection = (props: SideBarSectionProps) => { const user = useSelector(getUser); const applications = useSelector(normalAppListSelector); const currentPath = useLocation().pathname; - + const isShow = props.items.map(item => item.visible ? item.visible({ user: user, applications: applications }) : true).includes(true); return ( - + {props.title} {props.items .filter((item) => diff --git a/client/packages/lowcoder/src/components/table/columnTypeView.tsx b/client/packages/lowcoder/src/components/table/columnTypeView.tsx index 434e71551..87216d833 100644 --- a/client/packages/lowcoder/src/components/table/columnTypeView.tsx +++ b/client/packages/lowcoder/src/components/table/columnTypeView.tsx @@ -38,7 +38,7 @@ const ColumnTypeHoverView = styled.div<{ max-height: 150px; max-width: 300px; overflow: auto; - background: inherit; + background: #fafafa; z-index: 3; padding: ${(props) => props.$padding}; top: ${(props) => `${props.$adjustTop || 0}px`}; diff --git a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx index 05e8eed96..a2b54ddc8 100644 --- a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx @@ -216,7 +216,7 @@ const childrenMap = { lowcoderCompVersion: withDefault(StringControl, 'latest'), maxWidth: dropdownInputSimpleControl(OPTIONS, USER_DEFINE, "1920"), gridColumns: RangeControl.closed(8, 48, 24), - gridRowHeight: RangeControl.closed(6, 20, 8), + gridRowHeight: RangeControl.closed(4, 100, 8), gridRowCount: withDefault(NumberControl, DEFAULT_ROW_COUNT), gridPaddingX: withDefault(NumberControl, 20), gridPaddingY: withDefault(NumberControl, 20), diff --git a/client/packages/lowcoder/src/comps/comps/containerComp/containerView.tsx b/client/packages/lowcoder/src/comps/comps/containerComp/containerView.tsx index 1e47c5703..9af5096d3 100644 --- a/client/packages/lowcoder/src/comps/comps/containerComp/containerView.tsx +++ b/client/packages/lowcoder/src/comps/comps/containerComp/containerView.tsx @@ -62,6 +62,7 @@ import { selectCompModifierKeyPressed } from "util/keyUtils"; import { defaultLayout, GridItemComp, GridItemDataType } from "../gridItemComp"; import { ThemeContext } from "comps/utils/themeContext"; import { defaultTheme } from "@lowcoder-ee/constants/themeConstants"; +import { ExpandViewContext } from "../tableComp/expansionControl"; const childrenMap = { layout: valueComp({}), @@ -357,11 +358,12 @@ export const InnerGrid = React.memo((props: ViewPropsWithSelect) => { || String(DEFAULT_GRID_COLUMNS); }, [horizontalGridCells, positionParams.cols]); + const isExpandView = useContext(ExpandViewContext); const isDroppable = - useContext(IsDroppable) && (_.isNil(props.isDroppable) || props.isDroppable) && !readOnly; - const isDraggable = !readOnly && (_.isNil(props.isDraggable) || props.isDraggable); - const isResizable = !readOnly && (_.isNil(props.isResizable) || props.isResizable); - const isSelectable = !readOnly && (_.isNil(props.isSelectable) || props.isSelectable); + useContext(IsDroppable) && (_.isNil(props.isDroppable) || props.isDroppable) && !readOnly && !isExpandView; + const isDraggable = !readOnly && !isExpandView && (_.isNil(props.isDraggable) || props.isDraggable); + const isResizable = !readOnly && !isExpandView && (_.isNil(props.isResizable) || props.isResizable); + const isSelectable = !readOnly && !isExpandView && (_.isNil(props.isSelectable) || props.isSelectable); const extraLayout = useMemo( () => getExtraLayout( @@ -484,7 +486,7 @@ export const InnerGrid = React.memo((props: ViewPropsWithSelect) => { setRowCount(Infinity); onRowCountChange?.(0); } - }, [isRowCountLocked, onRowCountChange]); + }, [isRowCountLocked, positionParams.rowHeight, onRowCountChange]); // log.info("rowCount:", currentRowCount, "rowHeight:", currentRowHeight); diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx index 62f84bf84..686153fd2 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx @@ -50,6 +50,8 @@ import { DateRangeUIView } from "comps/comps/dateComp/dateRangeUIView"; import { EditorContext } from "comps/editorState"; import { dropdownControl } from "comps/controls/dropdownControl"; import { timeZoneOptions } from "./timeZone"; +import { migrateOldData } from "@lowcoder-ee/comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; @@ -142,6 +144,7 @@ function validate( } const childrenMap = { + defaultValue: stringExposingStateControl("defaultValue"), value: stringExposingStateControl("value"), userTimeZone: stringExposingStateControl("userTimeZone", Intl.DateTimeFormat().resolvedOptions().timeZone), ...commonChildren, @@ -170,18 +173,25 @@ export type DateCompViewProps = Pick< placeholder?: string | [string, string]; }; -export const datePickerControl = new UICompBuilder(childrenMap, (props) => { +const DatePickerTmpCmp = new UICompBuilder(childrenMap, (props) => { + const defaultValue = { ...props.defaultValue }.value; + const value = { ...props.value }.value; + let time: dayjs.Dayjs | null = null; - if (props.value.value !== '') { - time = dayjs(props.value.value, DateParser); + if (value !== '') { + time = dayjs(value, DateParser); } const [tempValue, setTempValue] = useState(time); useEffect(() => { - const value = props.value.value ? dayjs(props.value.value, DateParser) : null; - setTempValue(value); - }, [props.value.value]) + props.value.onChange(defaultValue); + }, [defaultValue]); + + useEffect(() => { + const newValue = value ? dayjs(value, DateParser) : null; + setTempValue(newValue); + }, [value]) const handleDateZoneChange = (newTimeZone: any) => { props.userTimeZone.onChange(newTimeZone) @@ -234,7 +244,7 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { return ( <>
- {children.value.propertyView({ + {children.defaultValue.propertyView({ label: trans("prop.defaultValue"), placeholder: "2022-04-07 21:39:59", tooltip: trans("date.formatTip") @@ -304,9 +314,33 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { .setExposeMethodConfigs(dateRefMethods) .build(); -export const dateRangeControl = (function () { +export const datePickerControl = migrateOldData(DatePickerTmpCmp, fixOldInputCompData); + +export function fixOldDateOrTimeRangeData(oldData: any) { + if (!oldData) return oldData; + + let {defaultStart, defaultEnd} = oldData + if (Boolean(oldData.start) && !Boolean(oldData.defaultStart)) { + defaultStart = oldData.start; + } + if (Boolean(oldData.end) && !Boolean(oldData.defaultEnd)) { + defaultEnd = oldData.end; + } + return { + ...oldData, + defaultStart, + defaultEnd, + start: '', + end: '', + }; + // return oldData; +} + +let DateRangeTmpCmp = (function () { const childrenMap = { + defaultStart: stringExposingStateControl("defaultStart"), start: stringExposingStateControl("start"), + defaultEnd: stringExposingStateControl("defaultEnd"), end: stringExposingStateControl("end"), userRangeTimeZone: stringExposingStateControl("userRangeTimeZone" , Intl.DateTimeFormat().resolvedOptions().timeZone), ...formDataChildren, @@ -314,28 +348,42 @@ export const dateRangeControl = (function () { }; return new UICompBuilder(childrenMap, (props) => { + const defaultStart = { ...props.defaultStart }.value; + const startValue = { ...props.start }.value; + + const defaultEnd = { ...props.defaultEnd }.value; + const endValue = { ...props.end }.value; + let start: dayjs.Dayjs | null = null; - if (props.start.value !== '') { - start = dayjs(props.start.value, DateParser); + if (startValue !== '') { + start = dayjs(startValue, DateParser); } let end: dayjs.Dayjs | null = null; - if (props.end.value !== '') { - end = dayjs(props.end.value, DateParser); + if (endValue !== '') { + end = dayjs(endValue, DateParser); } const [tempStartValue, setTempStartValue] = useState(start); const [tempEndValue, setTempEndValue] = useState(end); useEffect(() => { - const value = props.start.value ? dayjs(props.start.value, DateParser) : null; + props.start.onChange(defaultStart); + }, [defaultStart]); + + useEffect(() => { + props.end.onChange(defaultEnd); + }, [defaultEnd]); + + useEffect(() => { + const value = startValue ? dayjs(startValue, DateParser) : null; setTempStartValue(value); - }, [props.start.value]) + }, [startValue]) useEffect(() => { - const value = props.end.value ? dayjs(props.end.value, DateParser) : null; + const value = endValue ? dayjs(endValue, DateParser) : null; setTempEndValue(value); - }, [props.end.value]) + }, [endValue]) const handleDateRangeZoneChange = (newTimeZone: any) => { @@ -399,12 +447,12 @@ export const dateRangeControl = (function () { return ( <>
- {children.start.propertyView({ + {children.defaultStart.propertyView({ label: trans("date.start"), placeholder: "2022-04-07 21:39:59", tooltip: trans("date.formatTip"), })} - {children.end.propertyView({ + {children.defaultEnd.propertyView({ label: trans("date.end"), placeholder: "2022-04-07 21:39:59", tooltip: trans("date.formatTip"), @@ -471,6 +519,8 @@ export const dateRangeControl = (function () { .build(); })(); +export const dateRangeControl = migrateOldData(DateRangeTmpCmp, fixOldDateOrTimeRangeData); + const getTimeZoneInfo = (timeZone: any, otherTimeZone: any) => { const tz = timeZone === 'UserChoice' ? otherTimeZone : timeZone; diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx index 4d9f0a6e5..7a578f5a5 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx @@ -54,7 +54,9 @@ import { TimePickerProps } from "antd/es/time-picker"; import { EditorContext } from "comps/editorState"; import { dropdownControl } from "comps/controls/dropdownControl"; import { timeZoneOptions } from "./timeZone"; - +import { migrateOldData } from "@lowcoder-ee/comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; +import { fixOldDateOrTimeRangeData } from "./dateComp"; const EventOptions = [changeEvent, focusEvent, blurEvent] as const; @@ -124,6 +126,7 @@ function validate( } const childrenMap = { + defaultValue: stringExposingStateControl("defaultValue"), value: stringExposingStateControl("value"), userTimeZone: stringExposingStateControl("userTimeZone", Intl.DateTimeFormat().resolvedOptions().timeZone), ...commonChildren, @@ -149,18 +152,25 @@ export type TimeCompViewProps = Pick< timeZone:string }; -export const timePickerControl = new UICompBuilder(childrenMap, (props) => { +const TimePickerTmpCmp = new UICompBuilder(childrenMap, (props) => { + const defaultValue = { ...props.defaultValue }.value; + const value = { ...props.value }.value; + let time: dayjs.Dayjs | null = null; - if(props.value.value !== '') { - time = dayjs(props.value.value, TimeParser); + if(value !== '') { + time = dayjs(value, TimeParser); } const [tempValue, setTempValue] = useState(time); useEffect(() => { - const value = props.value.value ? dayjs(props.value.value, TimeParser) : null; - setTempValue(value); - }, [props.value.value]) + props.value.onChange(defaultValue); + }, [defaultValue]); + + useEffect(() => { + const newValue = value ? dayjs(value, TimeParser) : null; + setTempValue(newValue); + }, [value]) const handleTimeZoneChange = (newTimeZone: any) => { props.userTimeZone.onChange(newTimeZone) @@ -205,7 +215,7 @@ export const timePickerControl = new UICompBuilder(childrenMap, (props) => { .setPropertyViewFn((children) => ( <>
- {children.value.propertyView({ + {children.defaultValue.propertyView({ label: trans("prop.defaultValue"), tooltip: trans("time.formatTip"), })} @@ -270,9 +280,13 @@ export const timePickerControl = new UICompBuilder(childrenMap, (props) => { .setExposeMethodConfigs(dateRefMethods) .build(); -export const timeRangeControl = (function () { +export const timePickerControl = migrateOldData(TimePickerTmpCmp, fixOldInputCompData); + +const TimeRangeTmpCmp = (function () { const childrenMap = { + defaultStart: stringExposingStateControl("defaultStart"), start: stringExposingStateControl("start"), + defaultEnd: stringExposingStateControl("defaultEnd"), end: stringExposingStateControl("end"), userRangeTimeZone: stringExposingStateControl("userRangeTimeZone" , Intl.DateTimeFormat().resolvedOptions().timeZone), ...formDataChildren, @@ -280,27 +294,41 @@ export const timeRangeControl = (function () { }; return new UICompBuilder(childrenMap, (props) => { + const defaultStart = { ...props.defaultStart }.value; + const startValue = { ...props.start }.value; + + const defaultEnd = { ...props.defaultEnd }.value; + const endValue = { ...props.end }.value; + let start: dayjs.Dayjs | null = null; - if(props.start.value !== '') { - start = dayjs(props.start.value, TimeParser); + if(startValue !== '') { + start = dayjs(startValue, TimeParser); } let end: dayjs.Dayjs | null = null; - if(props.end.value !== '') { - end = dayjs(props.end.value, TimeParser); + if(endValue !== '') { + end = dayjs(endValue, TimeParser); } const [tempStartValue, setTempStartValue] = useState(start); const [tempEndValue, setTempEndValue] = useState(end); useEffect(() => { - const value = props.start.value ? dayjs(props.start.value, TimeParser) : null; + props.start.onChange(defaultStart); + }, [defaultStart]); + + useEffect(() => { + props.end.onChange(defaultEnd); + }, [defaultEnd]); + + useEffect(() => { + const value = startValue ? dayjs(startValue, TimeParser) : null; setTempStartValue(value); - }, [props.start.value]) + }, [startValue]) useEffect(() => { - const value = props.end.value ? dayjs(props.end.value, TimeParser) : null; + const value = endValue ? dayjs(endValue, TimeParser) : null; setTempEndValue(value); - }, [props.end.value]) + }, [endValue]) const handleTimeRangeZoneChange = (newTimeZone: any) => { props.userRangeTimeZone.onChange(newTimeZone) @@ -354,11 +382,11 @@ export const timeRangeControl = (function () { .setPropertyViewFn((children) => ( <>
- {children.start.propertyView({ + {children.defaultStart.propertyView({ label: trans("time.start"), tooltip: trans("time.formatTip"), })} - {children.end.propertyView({ + {children.defaultEnd.propertyView({ label: trans("time.end"), tooltip: trans("time.formatTip"), })} @@ -423,6 +451,8 @@ export const timeRangeControl = (function () { .build(); })(); +export const timeRangeControl = migrateOldData(TimeRangeTmpCmp, fixOldDateOrTimeRangeData); + const getTimeZoneInfo = (timeZone: any, otherTimeZone: any) => { const tz = timeZone === 'UserChoice' ? otherTimeZone : timeZone; diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx index 5ce11399e..def8c27e6 100644 --- a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx @@ -1,6 +1,7 @@ import { default as Button } from "antd/es/button"; import { default as AntdUpload } from "antd/es/upload"; -import { UploadFile, UploadProps, UploadChangeParam } from "antd/es/upload/interface"; +import { default as Dropdown } from "antd/es/dropdown"; +import { UploadFile, UploadProps, UploadChangeParam, UploadFileStatus, RcFile } from "antd/es/upload/interface"; import { Buffer } from "buffer"; import { darkenColor } from "components/colorSelect/colorUtils"; import { Section, sectionNames } from "components/Section"; @@ -22,7 +23,7 @@ import { RecordConstructorToView, } from "lowcoder-core"; import { UploadRequestOption } from "rc-upload/lib/interface"; -import { useEffect, useState } from "react"; +import { Suspense, useCallback, useEffect, useRef, useState } from "react"; import styled, { css } from "styled-components"; import { JSONObject, JSONValue } from "../../../util/jsonTypes"; import { BoolControl, BoolPureControl } from "../../controls/boolControl"; @@ -39,9 +40,15 @@ import { stateComp, UICompBuilder, withDefault } from "../../generators"; import { CommonNameConfig, NameConfig, withExposingConfigs } from "../../generators/withExposing"; import { formDataChildren, FormDataPropertyView } from "../formComp/formDataConstants"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; +import { CustomModal } from "lowcoder-design"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import type { ItemType } from "antd/es/menu/interface"; +import Skeleton from "antd/es/skeleton"; +import Menu from "antd/es/menu"; +import Flex from "antd/es/flex"; +import { checkIsMobile } from "@lowcoder-ee/index.sdk"; const FileSizeControl = codeControl((value) => { if (typeof value === "number") { @@ -106,6 +113,7 @@ const commonChildren = { parsedValue: stateComp>([]), prefixIcon: withDefault(IconControl, "/icon:solid/arrow-up-from-bracket"), suffixIcon: IconControl, + forceCapture: BoolControl, ...validationChildren, }; @@ -202,6 +210,46 @@ const IconWrapper = styled.span` display: flex; `; +const CustomModalStyled = styled(CustomModal)` + top: 10vh; + .react-draggable { + max-width: 100%; + width: 500px; + + video { + width: 100%; + } + } +`; + +const Error = styled.div` + color: #f5222d; + height: 100px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +`; + +const Wrapper = styled.div` + img, + video, + .ant-skeleton { + width: 100%; + height: 400px; + max-height: 70vh; + position: relative; + object-fit: cover; + background-color: #000; + } + .ant-skeleton { + h3, + li { + background-color: transparent; + } + } +`; + export function resolveValue(files: UploadFile[]) { return Promise.all( files.map( @@ -241,17 +289,174 @@ export function resolveParsedValue(files: UploadFile[]) { ); } +const ReactWebcam = React.lazy(() => import("react-webcam")); + +const ImageCaptureModal = (props: { + showModal: boolean, + onModalClose: () => void; + onImageCapture: (image: string) => void; +}) => { + const [errMessage, setErrMessage] = useState(""); + const [videoConstraints, setVideoConstraints] = useState({ + facingMode: "environment", + }); + const [modeList, setModeList] = useState([]); + const [dropdownShow, setDropdownShow] = useState(false); + const [imgSrc, setImgSrc] = useState(); + const webcamRef = useRef(null); + + useEffect(() => { + if (props.showModal) { + setImgSrc(''); + setErrMessage(''); + } + }, [props.showModal]); + + const handleMediaErr = (err: any) => { + if (typeof err === "string") { + setErrMessage(err); + } else { + if (err.message === "getUserMedia is not implemented in this browser") { + setErrMessage(trans("scanner.errTip")); + } else { + setErrMessage(err.message); + } + } + }; + + const handleCapture = useCallback(() => { + const imageSrc = webcamRef.current?.getScreenshot?.(); + setImgSrc(imageSrc); + }, [webcamRef]); + + const getModeList = () => { + navigator.mediaDevices.enumerateDevices().then((data) => { + const videoData = data.filter((item) => item.kind === "videoinput"); + const faceModeList = videoData.map((item, index) => ({ + label: item.label || trans("scanner.camera", { index: index + 1 }), + key: item.deviceId, + })); + setModeList(faceModeList); + }); + }; + + return ( + + {!!errMessage ? ( + {errMessage} + ) : ( + props.showModal && ( + + {imgSrc + ? webcam + : ( + }> + + + ) + } + {imgSrc + ? ( + + + + + ) + : ( + + + setDropdownShow(value)} + dropdownRender={() => ( + + setVideoConstraints({ ...videoConstraints, deviceId: value.key }) + } + /> + )} + > + + + + ) + } + + ) + )} + + ) +} + const Upload = ( props: RecordConstructorToView & { uploadType: "single" | "multiple" | "directory"; text: string; dispatch: (action: CompAction) => void; - } + forceCapture: boolean; + }, ) => { const { dispatch, files, style } = props; const [fileList, setFileList] = useState( files.map((f) => ({ ...f, status: "done" })) as UploadFile[] ); + const [showModal, setShowModal] = useState(false); + const isMobile = checkIsMobile(window.innerWidth); + useEffect(() => { if (files.length === 0 && fileList.length !== 0) { setFileList([]); @@ -259,110 +464,146 @@ const Upload = ( }, [files]); // chrome86 bug: button children should not contain only empty span const hasChildren = hasIcon(props.prefixIcon) || !!props.text || hasIcon(props.suffixIcon); + + const handleOnChange = (param: UploadChangeParam) => { + const uploadingFiles = param.fileList.filter((f) => f.status === "uploading"); + // the onChange callback will be executed when the state of the antd upload file changes. + // so make a trick logic: the file list with loading will not be processed + if (uploadingFiles.length !== 0) { + setFileList(param.fileList); + return; + } + + let maxFiles = props.maxFiles; + if (props.uploadType === "single") { + maxFiles = 1; + } else if (props.maxFiles <= 0) { + maxFiles = 100; // limit 100 currently + } + + const uploadedFiles = param.fileList.filter((f) => f.status === "done"); + + if (param.file.status === "removed") { + const index = props.files.findIndex((f) => f.uid === param.file.uid); + dispatch( + multiChangeAction({ + value: changeValueAction( + [...props.value.slice(0, index), ...props.value.slice(index + 1)], + false + ), + files: changeValueAction( + [...props.files.slice(0, index), ...props.files.slice(index + 1)], + false + ), + parsedValue: changeValueAction( + [...props.parsedValue.slice(0, index), ...props.parsedValue.slice(index + 1)], + false + ), + }) + ); + props.onEvent("change"); + } else { + const unresolvedValueIdx = Math.min(props.value.length, uploadedFiles.length); + const unresolvedParsedValueIdx = Math.min(props.parsedValue.length, uploadedFiles.length); + + // After all files are processed, perform base64 encoding on the latest file list uniformly + Promise.all([ + resolveValue(uploadedFiles.slice(unresolvedValueIdx)), + resolveParsedValue(uploadedFiles.slice(unresolvedParsedValueIdx)), + ]).then(([value, parsedValue]) => { + dispatch( + multiChangeAction({ + value: changeValueAction([...props.value, ...value].slice(-maxFiles), false), + files: changeValueAction( + uploadedFiles + .map((file) => _.pick(file, ["uid", "name", "type", "size", "lastModified"])) + .slice(-maxFiles), + false + ), + ...(props.parseFiles + ? { + parsedValue: changeValueAction( + [...props.parsedValue, ...parsedValue].slice(-maxFiles), + false + ), + } + : {}), + }) + ); + props.onEvent("change"); + props.onEvent("parse"); + }); + } + + setFileList(uploadedFiles.slice(-maxFiles)); + }; + return ( - { - if (!file.size || file.size <= 0) { - messageInstance.error(`${file.name} ` + trans("file.fileEmptyErrorMsg")); - return AntdUpload.LIST_IGNORE; - } - - if ( - (!!props.minSize && file.size < props.minSize) || - (!!props.maxSize && file.size > props.maxSize) - ) { - messageInstance.error(`${file.name} ` + trans("file.fileSizeExceedErrorMsg")); - return AntdUpload.LIST_IGNORE; - } - return true; - }} - onChange={(param: UploadChangeParam) => { - const uploadingFiles = param.fileList.filter((f) => f.status === "uploading"); - // the onChange callback will be executed when the state of the antd upload file changes. - // so make a trick logic: the file list with loading will not be processed - if (uploadingFiles.length !== 0) { - setFileList(param.fileList); - return; - } - - let maxFiles = props.maxFiles; - if (props.uploadType === "single") { - maxFiles = 1; - } else if (props.maxFiles <= 0) { - maxFiles = 100; // limit 100 currently - } - - const uploadedFiles = param.fileList.filter((f) => f.status === "done"); - - if (param.file.status === "removed") { - const index = props.files.findIndex((f) => f.uid === param.file.uid); - dispatch( - multiChangeAction({ - value: changeValueAction( - [...props.value.slice(0, index), ...props.value.slice(index + 1)], - false - ), - files: changeValueAction( - [...props.files.slice(0, index), ...props.files.slice(index + 1)], - false - ), - parsedValue: changeValueAction( - [...props.parsedValue.slice(0, index), ...props.parsedValue.slice(index + 1)], - false - ), - }) - ); - props.onEvent("change"); - } else { - const unresolvedValueIdx = Math.min(props.value.length, uploadedFiles.length); - const unresolvedParsedValueIdx = Math.min(props.parsedValue.length, uploadedFiles.length); - - // After all files are processed, perform base64 encoding on the latest file list uniformly - Promise.all([ - resolveValue(uploadedFiles.slice(unresolvedValueIdx)), - resolveParsedValue(uploadedFiles.slice(unresolvedParsedValueIdx)), - ]).then(([value, parsedValue]) => { - dispatch( - multiChangeAction({ - value: changeValueAction([...props.value, ...value].slice(-maxFiles), false), - files: changeValueAction( - uploadedFiles - .map((file) => _.pick(file, ["uid", "name", "type", "size", "lastModified"])) - .slice(-maxFiles), - false - ), - ...(props.parseFiles - ? { - parsedValue: changeValueAction( - [...props.parsedValue, ...parsedValue].slice(-maxFiles), - false - ), - } - : {}), - }) - ); - props.onEvent("change"); - props.onEvent("parse"); - }); - } + <> + { + if (!file.size || file.size <= 0) { + messageInstance.error(`${file.name} ` + trans("file.fileEmptyErrorMsg")); + return AntdUpload.LIST_IGNORE; + } - setFileList(uploadedFiles.slice(-maxFiles)); - }} - > - - + if ( + (!!props.minSize && file.size < props.minSize) || + (!!props.maxSize && file.size > props.maxSize) + ) { + messageInstance.error(`${file.name} ` + trans("file.fileSizeExceedErrorMsg")); + return AntdUpload.LIST_IGNORE; + } + return true; + }} + onChange={handleOnChange} + + > + + + + setShowModal(false)} + onImageCapture={async (image) => { + setShowModal(false); + const res: Response = await fetch(image); + const blob: Blob = await res.blob(); + const file = new File([blob], "image.jpg", {type: 'image/jpeg'}); + const fileUid = uuid.v4(); + const uploadFile = { + uid: fileUid, + name: file.name, + type: file.type, + size: file.size, + lastModified: file.lastModified, + lastModifiedDate: (file as any).lastModifiedDate, + status: 'done' as UploadFileStatus, + originFileObj: file as RcFile, + }; + handleOnChange({file: uploadFile, fileList: [...fileList, uploadFile]}) + }} + /> + ); }; @@ -419,6 +660,10 @@ let FileTmpComp = new UICompBuilder(childrenMap, (props, dispatch) => { })} {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} {children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })} + {children.forceCapture.propertyView({ + label: trans("file.forceCapture"), + tooltip: trans("file.forceCaptureTooltip") + })} {children.showUploadList.propertyView({ label: trans("file.showUploadList") })} {children.parseFiles.propertyView({ label: trans("file.parseFiles"), diff --git a/client/packages/lowcoder/src/comps/comps/fileViewerComp.tsx b/client/packages/lowcoder/src/comps/comps/fileViewerComp.tsx index 1c553bca9..cba4c8aaf 100644 --- a/client/packages/lowcoder/src/comps/comps/fileViewerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileViewerComp.tsx @@ -10,9 +10,10 @@ import { UICompBuilder, withDefault } from "../generators"; import { NameConfig, NameConfigHidden, withExposingConfigs } from "../generators/withExposing"; import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; -import { AutoHeightControl, BoolControl } from "@lowcoder-ee/index.sdk"; import { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { AutoHeightControl } from "../controls/autoHeightControl"; +import { BoolControl } from "../controls/boolControl"; const getStyle = (style: FileViewerStyleType) => { return css` diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonEditorComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonEditorComp.tsx index 50f09db3c..3cc215c0b 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonEditorComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonEditorComp.tsx @@ -20,7 +20,8 @@ import { } from "base/codeEditor/codeMirror"; import { useExtensions } from "base/codeEditor/extensions"; import { EditorContext } from "comps/editorState"; -import { AutoHeightControl, BoolControl } from "@lowcoder-ee/index.sdk"; +import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl"; +import { BoolControl } from "@lowcoder-ee/comps/controls/boolControl"; /** * JsonEditor Comp diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonExplorerComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonExplorerComp.tsx index a42bb6cdf..0a9e5f886 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonExplorerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonExplorerComp.tsx @@ -13,7 +13,7 @@ import { EditorContext } from "comps/editorState"; import { useContext, useEffect } from "react"; import { AnimationStyle, AnimationStyleType } from "@lowcoder-ee/comps/controls/styleControlConstants"; import { styleControl } from "@lowcoder-ee/comps/controls/styleControl"; -import { AutoHeightControl } from "@lowcoder-ee/index.sdk"; +import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl"; /** * JsonExplorer Comp diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ArrayFieldTemplate.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ArrayFieldTemplate.tsx index 5af2ba32d..f5feb4d0a 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ArrayFieldTemplate.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ArrayFieldTemplate.tsx @@ -1,60 +1,114 @@ import React from 'react'; import { Button, Row, Col } from 'antd'; -import { ArrayFieldTemplateProps } from '@rjsf/utils'; +import { ArrayFieldTemplateProps, getUiOptions, RJSFSchema } from '@rjsf/utils'; import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; +import ObjectFieldTemplate from './ObjectFieldTemplate'; // Ensure this is correctly imported + +const DEFAULT_RESPONSIVE_COL_SPAN = { + xs: 24, + sm: 24, + md: 12, + lg: 8, + xl: 6, +}; + +type UiProps = { + rowGutter?: number; + colSpan?: number | Record; +}; const ArrayFieldTemplate = (props: ArrayFieldTemplateProps) => { - const { items, canAdd, onAddClick, title } = props; + const { items, canAdd, onAddClick, title, uiSchema, registry } = props; - return ( -
- {title && {title}} - - {items.map((element: any) => ( - - {/* Content container for the array item */} -
- {element.children} -
+ // Get UI schema configuration + const { rowGutter = 8, colSpan = DEFAULT_RESPONSIVE_COL_SPAN } = getUiOptions(uiSchema)?.["ui:props"] as UiProps || {}; - {/* Container for the control buttons with vertical alignment */} -
- {/* Move down button */} - {element.hasMoveDown && ( -
- - ))} - {/* Add button for the array */} + return ( + + {/* Use ObjectFieldTemplate to render each array item */} + void { + throw new Error('Function not implemented.'); + } } + /> + + {/* Control buttons */} +
+ {element.hasMoveDown && ( +
+ + ); + }); + }; + + return ( +
+ + + {renderItems()} {/* Render items */} {canAdd && ( - + @@ -65,4 +119,4 @@ const ArrayFieldTemplate = (props: ArrayFieldTemplateProps) => { ); }; -export default ArrayFieldTemplate; +export default ArrayFieldTemplate; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/LayoutFieldTemplate.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/LayoutFieldTemplate.tsx new file mode 100644 index 000000000..79049fad1 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/LayoutFieldTemplate.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import ObjectFieldTemplate from "./ObjectFieldTemplate"; // Import the existing ObjectFieldTemplate + +export const LayoutFieldTemplate = (props: any) => { + const { schema, uiSchema, children, ...rest } = props; // Spread to include all props + + // Handle custom layouts + switch (schema.type) { + case "Group": + return ( +
+

{schema.label || "Group"}

+ {children} +
+ ); + case "HorizontalLayout": + return
{children}
; + case "VerticalLayout": + return
{children}
; + default: + // Delegate to the existing ObjectFieldTemplate, ensuring all props are passed + return ; + } +}; + +export default LayoutFieldTemplate; diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx index 8791095c5..c03a436c9 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx @@ -1,7 +1,11 @@ -import React from 'react'; -import { Row, Col } from 'antd'; +import React, { useEffect, useRef, useState } from "react"; +import { Row, Col, Tabs } from 'antd'; import { ObjectFieldTemplateProps, getTemplate, getUiOptions, descriptionId, titleId, canExpand } from '@rjsf/utils'; import { ConfigConsumer } from 'antd/es/config-provider/context'; +import { useContainerWidth } from "./jsonSchemaFormComp"; +import styled from "styled-components"; +import TabPane from "antd/es/tabs/TabPane"; +import { is } from "core-js/core/object"; const DESCRIPTION_COL_STYLE = { paddingBottom: '8px', @@ -21,7 +25,7 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { readonly, registry, } = props; - + const containerWidth = useContainerWidth(); const uiOptions = getUiOptions(uiSchema); const TitleFieldTemplate = getTemplate('TitleFieldTemplate', registry, uiOptions); const DescriptionFieldTemplate = getTemplate('DescriptionFieldTemplate', registry, uiOptions); @@ -38,58 +42,301 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { xl: 8, // Extra large devices }; - const { rowGutter = 4, colSpan = defaultResponsiveColSpan } = uiSchema?.['ui:props'] || {}; + const { rowGutter = 4 } = uiSchema?.['ui:props'] || {}; + + const getLegendStyle = (level: number): React.CSSProperties => { + switch (level) { + case 0: + return { fontSize: "16px", fontWeight: "bold", marginBottom: "8px" }; // Form Title + case 1: + return { fontSize: "14px", fontWeight: "600", marginBottom: "6px" }; // Section Title + default: + return { fontSize: "12px", fontWeight: "normal", marginBottom: "4px" }; // Field Title + } + }; + + const calculateResponsiveColSpan = (uiSchema: any = {}): { span: number } => { + const colSpan = uiSchema?.["ui:colSpan"] || { + xs: 24, + sm: 24, + md: 12, + lg: 12, + xl: 8, + }; + + if (typeof colSpan === "number") { + return { span: colSpan }; + } else if (typeof colSpan === "object") { + if (containerWidth > 1200 && colSpan.xl !== undefined) { + return { span: colSpan.xl }; + } else if (containerWidth > 992 && colSpan.lg !== undefined) { + return { span: colSpan.lg }; + } else if (containerWidth > 768 && colSpan.md !== undefined) { + return { span: colSpan.md }; + } else if (containerWidth > 576 && colSpan.sm !== undefined) { + return { span: colSpan.sm }; + } else if (colSpan.xs !== undefined) { + return { span: colSpan.xs }; + } + } + return { span: 24 }; // Default span + }; + + const getFieldRenderer = (type: string) => { + const typeMap: Record = { + string: "StringField", // Handles strings + number: "NumberField", // Handles floating-point numbers + integer: "NumberField", // Handles integers (mapped to NumberField) + boolean: "BooleanField", // Handles true/false values + object: "ObjectField", // Handles nested objects + array: "ArrayField", // Handles arrays + null: "NullField", // Handles null values + anyOf: "AnyOfField", // Handles anyOf schemas + oneOf: "OneOfField", // Handles oneOf schemas + schema: "SchemaField", + }; + + const fieldName = typeMap[type]; + return fieldName ? registry.fields[fieldName] : undefined; + }; + + const renderSingleLevel = (level : number) => { + return ( + + {properties.map((prop) => { + const isArray = prop.content.props.schema.type === "array"; + const colSpan = isArray + ? { span: 24 } + : calculateResponsiveColSpan(uiSchema?.[prop.name] || {}); + + return ( + + {/* Render legend for array fields */} + {isArray && ( + <>
+ {prop.content.props.schema.title} + + )} + {/* Render field content */} + {prop.content} + + ); + })} +
+ ); + }; - // Generate responsive colSpan props for each element - const calculateResponsiveColSpan = (element: any) => { - const { type } = element.content.props.schema; - const widget = getUiOptions(element.content.props.uiSchema).widget; + const renderCategorization = (elements: any[]) => { + return ( + + {elements.map((category, index) => ( + + {category.elements.map((element: any, elementIndex: number) => { + if (element.type === "HorizontalLayout") { + return ( + + {element.elements.map((field: any, fieldIndex: number) => { + const colSpan = calculateResponsiveColSpan(field.uiSchema); + return ( + + {properties.find((prop) => prop.name === field.scope.replace("#/properties/", "")) + ?.content} + + ); + })} + + ); + } - const defaultSpan = widget === 'textarea' || type === 'object' || type === 'array' ? 24 : colSpan; + if (element.type === "Control") { + return properties.find((prop) => prop.name === element.scope.replace("#/properties/", "")) + ?.content; + } - // Ensure the returned object is properly formatted for AntD responsive properties - return typeof defaultSpan === 'object' ? defaultSpan : { span: defaultSpan }; + return null; + })} + + ))} + + ); + }; + + const renderFieldsFromSection = (section: any, level: number = 0) => { + const { formData, schema, uiSchema } = section.content.props; + + if (schema.type === "object" && schema.properties) { + // Render fields for objects + const fieldKeys = Object.keys(schema.properties); + + return ( + + {fieldKeys.map((fieldKey) => { + const fieldSchema = schema.properties[fieldKey]; + const fieldUiSchema = uiSchema?.[fieldKey] || {}; + const fieldFormData = formData ? formData[fieldKey] : undefined; + const span = calculateResponsiveColSpan(fieldUiSchema); + + const FieldRenderer = getFieldRenderer(fieldSchema.type); + + if (!FieldRenderer) { + console.error(`No renderer found for field type: ${fieldSchema.type}`); + return ( + +
Unsupported field type: {fieldSchema.type}
+ + ); + } + + return ( + +
+ {fieldSchema.title || fieldKey} + { + section.content.props.onChange({ + ...formData, + [fieldKey]: value, + }); + }} + onBlur={section.content.props.onBlur} + onFocus={section.content.props.onFocus} + /> +
+ + ); + })} +
+ ); + } else if (schema.type === "array" && schema.items) { + // Render fields for arrays + const FieldRenderer = getFieldRenderer(schema.type); + + if (!FieldRenderer) { + console.error(`No renderer found for field type: ${schema.type}`); + return ( +
+

Unsupported field type: {schema.type}

+
+ ); + } + + return ( +
+ +
+ ); + } + + // Log error for unsupported or missing schema types + console.error("Unsupported or missing schema type in section:", section); + return null; + }; + + const renderSections = (properties: any[], level: number) => { + + const isMultiLevel = properties.some( + (prop) => prop.content.props.schema?.type === "object" && prop.content.props.schema?.properties + ); + + if (!isMultiLevel) { + return renderSingleLevel(level); + } + + return properties.map((section) => { + const schema = section.content.props.schema; + const isArray = typeof section.content.props.index === 'number'; + const sectionTitle = schema.title || section.name; + + return ( + + +
+ {/* Always render the legend for the section itself */} + {level === 0 && !isArray ? ( + {sectionTitle} + ) : null} + + {/* Render the section content */} + {renderFieldsFromSection(section, level + 1)} +
+ +
+ ); + }); }; return ( - {(configProps) => ( + {() => (
- - {schema.type === 'object' && title && ( - - - - )} - {description && ( + {/* Render Title */} + {schema.type === "object" && title && ( + + + + )} + + {/* Render Description */} + {description && ( + - + - )} - {uiSchema?.['ui:grid'] && Array.isArray(uiSchema['ui:grid']) ? ( - uiSchema['ui:grid'].map((ui_row: Record) => { - return Object.keys(ui_row).map((row_item) => { - const element = properties.find((p) => p.name === row_item); - return element ? ( - // Pass responsive colSpan props using the calculated values - - {element.content} - - ) : null; - }); - }) - ) : ( - properties.map((element) => ( - - {element.content} - - )) - )} - + + )} + + {/* Render Sections */} + {renderSections(properties,0)} + + {/* Expand Button */} {canExpand(schema, uiSchema, formData) && ( - + - + )} @@ -99,4 +346,4 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { ); }; -export default ObjectFieldTemplate; +export default ObjectFieldTemplate; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx index af2906d7d..c1e58eda8 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx @@ -22,19 +22,26 @@ import ErrorBoundary from "./errorBoundary"; import { Theme } from "@rjsf/antd"; import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { AutoHeightControl } from "../../controls/autoHeightControl"; -import { useContext, useEffect } from "react"; +import { useContext, useEffect, useRef, useState, createContext } from "react"; import { EditorContext } from "comps/editorState"; import ObjectFieldTemplate from './ObjectFieldTemplate'; import ArrayFieldTemplate from './ArrayFieldTemplate'; import { Select } from 'antd'; import Title from 'antd/es/typography/Title'; + Theme.widgets.DateWidget = DateWidget(false); Theme.widgets.DateTimeWidget = DateWidget(true); const Form = withTheme(Theme); const EventOptions = [submitEvent] as const; +const ContainerWidthContext = createContext(0); + +const useContainerWidth = () => { + return useContext(ContainerWidthContext); +}; + const Container = styled.div<{ $style: JsonSchemaFormStyleType; $animationStyle: AnimationStyleType; @@ -216,6 +223,7 @@ function onSubmit(props: { }); } + let FormBasicComp = (function () { const childrenMap = { resetAfterSubmit: BoolControl, @@ -228,6 +236,7 @@ let FormBasicComp = (function () { style: styleControl(JsonSchemaFormStyle , 'style'), animationStyle: styleControl(AnimationStyle , 'animationStyle'), }; + return new UICompBuilder(childrenMap, (props) => { // rjsf 4.20 supports ui:submitButtonOptions, but if the button is customized, it will not take effect. Here we implement it ourselves const buttonOptions = props?.uiSchema?.[ @@ -236,51 +245,83 @@ let FormBasicComp = (function () { const schema = props.schema; + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + // Monitor the container's width + useEffect(() => { + const updateWidth = () => { + if (containerRef.current) { + setContainerWidth(containerRef.current.offsetWidth); + } + }; + + const resizeObserver = new ResizeObserver(() => { + updateWidth(); + }); + + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + // Initial update + updateWidth(); + + // Cleanup observer on unmount + return () => { + resizeObserver.disconnect(); + }; + }, []); + return ( - - - - - {schema.title as string | number} - -
onSubmit(props)} - onChange={(e) => props.data.onChange(e.formData)} - transformErrors={(errors) => transformErrors(errors)} - templates={{ - ObjectFieldTemplate: ObjectFieldTemplate, - ArrayFieldTemplate: ArrayFieldTemplate, - }} - widgets={{ searchableSelect: SearchableSelectWidget }} - // ErrorList={ErrorList} - children={ - - } - /> - - - + + + + + + {schema.title as string | number} + + onSubmit(props)} + onChange={(e) => props.data.onChange(e.formData)} + transformErrors={(errors) => transformErrors(errors)} + templates={{ + ObjectFieldTemplate: ObjectFieldTemplate, + ArrayFieldTemplate: ArrayFieldTemplate, + // FieldTemplate: LayoutFieldTemplate, + }} + widgets={{ searchableSelect: SearchableSelectWidget }} + // ErrorList={ErrorList} + children={ + + } + /> + + + + + ); }) .setPropertyViewFn((children) => { @@ -439,5 +480,5 @@ FormTmpComp = withMethodExposing(FormTmpComp, [ }), }, ]); - export const JsonSchemaFormComp = FormTmpComp; +export { FormTmpComp, useContainerWidth }; diff --git a/client/packages/lowcoder/src/comps/comps/moduleComp/moduleComp.tsx b/client/packages/lowcoder/src/comps/comps/moduleComp/moduleComp.tsx index c880a81a1..c825c5b0d 100644 --- a/client/packages/lowcoder/src/comps/comps/moduleComp/moduleComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/moduleComp/moduleComp.tsx @@ -85,6 +85,7 @@ const childrenMap = { events: eventHandlerControl(), autoHeight: AutoHeightControl, scrollbars: withDefault(BoolControl, false), + loadModuleInDomWhenHide: withDefault(BoolControl, true), }; type DataType = ToDataType>; @@ -127,6 +128,9 @@ class ModuleTmpComp extends ModuleCompBase { label: trans("prop.scrollbar"), })} {hiddenPropertyView(this.children)} + {this.children.hidden.getView() && this.children.loadModuleInDomWhenHide.propertyView({ + label: "Load module in DOM when hidden", + })}
); @@ -525,6 +529,9 @@ const ModuleCompWithView = withViewFn(ModuleTmpComp, (comp) => { if (error) { return {error}; } + if (comp.children.hidden.getView() && !comp.children.loadModuleInDomWhenHide.getView()) { + return null; + } let content: ReactNode = appId ? : ; if (comp.moduleRootComp && comp.isReady) { diff --git a/client/packages/lowcoder/src/comps/comps/ratingComp.tsx b/client/packages/lowcoder/src/comps/comps/ratingComp.tsx index 6731200d4..4e915f1bc 100644 --- a/client/packages/lowcoder/src/comps/comps/ratingComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/ratingComp.tsx @@ -78,7 +78,7 @@ const RatingBasicComp = (function () { children: ( { props.value.onChange(e); changeRef.current = true; diff --git a/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx b/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx index 9fa889eb0..263400f76 100644 --- a/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx +++ b/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx @@ -1,4 +1,5 @@ -import { NPM_PLUGIN_ASSETS_BASE_URL } from "constants/npmPlugins"; +import { sdkConfig } from "@lowcoder-ee/constants/sdkConfig"; +import { ASSETS_BASE_URL, NPM_PLUGIN_ASSETS_BASE_URL } from "constants/npmPlugins"; import { trans } from "i18n"; import { CompConstructor } from "lowcoder-core"; import { @@ -17,7 +18,12 @@ async function npmLoader( // Falk: removed "packageVersion = "latest" as default value fir packageVersion - to ensure no automatic version jumping. const localPackageVersion = remoteInfo.packageVersion || "latest"; const { packageName, packageVersion, compName } = remoteInfo; - const entry = `${NPM_PLUGIN_ASSETS_BASE_URL}/${appId}/${packageName}@${localPackageVersion}/index.js`; + + const pluginBaseUrl = REACT_APP_BUNDLE_TYPE === 'sdk' && sdkConfig.baseURL + ? `${sdkConfig.baseURL}/${ASSETS_BASE_URL}` + : NPM_PLUGIN_ASSETS_BASE_URL; + + const entry = `${pluginBaseUrl}/${appId || 'none'}/${packageName}@${localPackageVersion}/index.js`; try { const module = await import( diff --git a/client/packages/lowcoder/src/comps/comps/remoteComp/remoteComp.tsx b/client/packages/lowcoder/src/comps/comps/remoteComp/remoteComp.tsx index da29dda9f..8399535d7 100644 --- a/client/packages/lowcoder/src/comps/comps/remoteComp/remoteComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/remoteComp/remoteComp.tsx @@ -16,7 +16,7 @@ import { CompContext } from "@lowcoder-ee/comps/utils/compContext"; import React from "react"; import type { AppState } from "@lowcoder-ee/redux/reducers"; import { useSelector } from "react-redux"; -import { useApplicationId } from "@lowcoder-ee/util/hooks"; +import { ExternalEditorContext } from "@lowcoder-ee/util/context/ExternalEditorContext"; const ViewError = styled.div` display: flex; @@ -63,7 +63,8 @@ const RemoteCompView = React.memo((props: React.PropsWithChildren(""); const editorState = useContext(EditorContext); const compState = useContext(CompContext); - const appId = useApplicationId(); + const externalEditorState = useContext(ExternalEditorContext); + const appId = externalEditorState.applicationId; const lowcoderCompPackageVersion = editorState?.getAppSettings().lowcoderCompVersion || 'latest'; const latestLowcoderCompsVersion = useSelector((state: AppState) => state.npmPlugin.packageVersion['lowcoder-comps']); diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx index 3dc80ed80..9278ec326 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx @@ -18,8 +18,8 @@ import { RefControl } from "comps/controls/refControl"; import { dropdownControl } from "comps/controls/dropdownControl"; import { useContext, useState, useEffect } from "react"; import { EditorContext } from "comps/editorState"; -import { AutoHeightControl } from "@lowcoder-ee/index.sdk"; import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; +import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl"; const sizeOptions = [ { diff --git a/client/packages/lowcoder/src/comps/comps/switchComp.tsx b/client/packages/lowcoder/src/comps/comps/switchComp.tsx index 0d7b727b8..00c5abaef 100644 --- a/client/packages/lowcoder/src/comps/comps/switchComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/switchComp.tsx @@ -16,6 +16,7 @@ import { trans } from "i18n"; import { RefControl } from "comps/controls/refControl"; import { refMethods } from "comps/generators/withMethodExposing"; import { blurMethod, clickMethod, focusWithOptions } from "comps/utils/methodUtils"; +import { fixOldInputCompData } from "./textInputComp/textInputConstants"; import { useContext, useEffect } from "react"; import { EditorContext } from "comps/editorState"; @@ -88,6 +89,7 @@ function fixOldData(oldData: any) { */ let SwitchTmpComp = (function () { const childrenMap = { + defaultValue: booleanExposingStateControl("defaultValue"), value: booleanExposingStateControl("value"), label: LabelControl, onEvent: eventHandlerControl(EventOptions), @@ -105,6 +107,13 @@ let SwitchTmpComp = (function () { ...formDataChildren, }; return new UICompBuilder(childrenMap, (props) => { + const defaultValue = { ...props.defaultValue }.value; + const value = { ...props.value }.value; + + useEffect(() => { + props.value.onChange(defaultValue); + }, [defaultValue]); + return props.label({ style: props.style, labelStyle: props.labelStyle, @@ -113,7 +122,7 @@ let SwitchTmpComp = (function () { children: ( { @@ -130,7 +139,7 @@ let SwitchTmpComp = (function () { return ( <>
- {children.value.propertyView({ label: trans("switchComp.defaultValue") })} + {children.defaultValue.propertyView({ label: trans("switchComp.defaultValue") })}
@@ -170,6 +179,8 @@ let SwitchTmpComp = (function () { .build(); })(); +SwitchTmpComp = migrateOldData(SwitchTmpComp, fixOldInputCompData); + export const SwitchComp = withExposingConfigs(SwitchTmpComp, [ new NameConfig("value", trans("switchComp.valueDesc")), ...CommonNameConfig, diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/expansionControl.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/expansionControl.tsx index 51905a186..fb2da12fe 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/expansionControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/expansionControl.tsx @@ -11,7 +11,7 @@ import { BackgroundColorContext } from "comps/utils/backgroundColorContext"; import { trans } from "i18n"; import _ from "lodash"; import { ConstructorToView, wrapChildAction } from "lowcoder-core"; -import { useContext } from "react"; +import { createContext, useContext } from "react"; import { tryToNumber } from "util/convertUtils"; import { SimpleContainerComp } from "../containerBase/simpleContainerComp"; import { OB_ROW_ORI_INDEX, RecordType } from "./tableUtils"; @@ -19,6 +19,7 @@ import { NameGenerator } from "comps/utils"; import { JSONValue } from "util/jsonTypes"; const ContextSlotControl = withSelectedMultiContext(SlotControl); +export const ExpandViewContext = createContext(false); const ContainerView = (props: ContainerBaseProps) => { return ; @@ -85,7 +86,11 @@ export class ExpansionControl extends ExpansionControlTmp { String(record[OB_ROW_ORI_INDEX]) ); const containerProps = slotControl.children.container.getView(); - return ; + return ( + + + + ); }, }, expandModalView: selectedContainer.getView(), diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx index 602b8fde2..fa05cd2e6 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx @@ -32,7 +32,7 @@ import { withMethodExposing } from "comps/generators/withMethodExposing"; import { MAP_KEY } from "comps/generators/withMultiContext"; import { NameGenerator } from "comps/utils"; import { trans } from "i18n"; -import _ from "lodash"; +import _, { isArray } from "lodash"; import { changeChildAction, CompAction, @@ -46,6 +46,7 @@ import { RecordNode, RecordNodeToValue, routeByNameAction, + ValueAndMsg, withFunction, wrapChildAction, } from "lowcoder-core"; @@ -55,7 +56,7 @@ import { lastValueIfEqual, shallowEqual } from "util/objectUtils"; import { IContainer } from "../containerBase"; import { getSelectedRowKeys } from "./selectionControl"; import { compTablePropertyView } from "./tablePropertyView"; -import { RowColorComp, RowHeightComp, TableChildrenView, TableInitComp } from "./tableTypes"; +import { RowColorComp, RowHeightComp, SortValue, TableChildrenView, TableInitComp } from "./tableTypes"; import { useContext, useState } from "react"; import { EditorContext } from "comps/editorState"; @@ -295,21 +296,50 @@ export class TableImplComp extends TableInitComp implements IContainer { // handle sort: data -> sortedData sortDataNode() { - const nodes = { + const nodes: { + data: Node; + sort: Node; + dataIndexes: RecordNode>>; + sortables: RecordNode>>>; + withParams: RecordNode<_.Dictionary>, + } = { data: this.children.data.exposingNode(), sort: this.children.sort.node(), dataIndexes: this.children.columns.getColumnsNode("dataIndex"), sortables: this.children.columns.getColumnsNode("sortable"), + withParams: this.children.columns.withParamsNode(), }; const sortedDataNode = withFunction(fromRecord(nodes), (input) => { const { data, sort, dataIndexes, sortables } = input; - const columns = _(dataIndexes) + const sortColumns = _(dataIndexes) .mapValues((dataIndex, idx) => ({ sortable: !!sortables[idx] })) .mapKeys((sortable, idx) => dataIndexes[idx]) .value(); - const sortedData = sortData(data, columns, sort); + const dataColumns = _(dataIndexes) + .mapValues((dataIndex, idx) => ({ + dataIndex, + render: input.withParams[idx] as any, + })) + .value(); + const updatedData: Array = data.map((row, index) => ({ + ...row, + [OB_ROW_ORI_INDEX]: index + "", + })); + const updatedDataMap: Record = {}; + updatedData.forEach((row) => { + updatedDataMap[row[OB_ROW_ORI_INDEX]] = row; + }) + const originalData = getOriDisplayData(updatedData, 1000, Object.values(dataColumns)) + const sortedData = sortData(originalData, sortColumns, sort); + // console.info( "sortNode. data: ", data, " sort: ", sort, " columns: ", columns, " sortedData: ", sortedData); - return sortedData; + const newData = sortedData.map(row => { + return { + ...row, + ...updatedDataMap[row[OB_ROW_ORI_INDEX]], + } + }); + return newData; }); return lastValueIfEqual(this, "sortedDataNode", [sortedDataNode, nodes] as const, (a, b) => shallowEqual(a[1], b[1]) @@ -631,6 +661,24 @@ TableTmpComp = withMethodExposing(TableTmpComp, [ } }, }, + { + method: { + name: "setMultiSort", + description: "", + params: [ + { name: "sortColumns", type: "arrayObject"}, + ], + }, + execute: (comp, values) => { + const sortColumns = values[0]; + if (!isArray(sortColumns)) { + return Promise.reject("setMultiSort function only accepts array of sort objects i.e. [{column: column_name, desc: boolean}]") + } + if (sortColumns && isArray(sortColumns)) { + comp.children.sort.dispatchChangeValueAction(sortColumns as SortValue[]); + } + }, + }, { method: { name: "resetSelections", @@ -817,6 +865,18 @@ export const TableComp = withExposingConfigs(TableTmpComp, [ }, trans("table.sortColumnDesc") ), + new DepsConfig( + "sortColumns", + (children) => { + return { + sort: children.sort.node(), + }; + }, + (input) => { + return input.sort; + }, + trans("table.sortColumnDesc") + ), depsConfig({ name: "sortDesc", desc: trans("table.sortDesc"), diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx index e6182a269..bca13e0aa 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx @@ -340,7 +340,7 @@ export function columnsToAntdFormat( status: StatusType; }[]; const title = renderTitle({ title: column.title, tooltip: column.titleTooltip, editable: column.editable }); - + return { key: `${column.dataIndex}-${mIndex}`, title: column.showTitle ? title : '', @@ -399,7 +399,7 @@ export function columnsToAntdFormat( }, ...(column.sortable ? { - sorter: true, + sorter: { multiple: (sortedColumns.length - mIndex) + 1 }, sortOrder: sortMap.get(column.dataIndex), showSorterTooltip: false, } diff --git a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx index a1fe29716..e062ba3bb 100644 --- a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx @@ -61,6 +61,7 @@ const childrenMap = { onEvent: eventHandlerControl(EVENT_OPTIONS), disabled: BoolCodeControl, showHeader: withDefault(BoolControl, true), + destroyInactiveTab: withDefault(BoolControl, false), style: styleControl(TabContainerStyle , 'style'), headerStyle: styleControl(ContainerHeaderStyle , 'headerStyle'), bodyStyle: styleControl(TabBodyStyle , 'bodyStyle'), @@ -196,6 +197,7 @@ const TabbedContainer = (props: TabbedContainerProps) => { headerStyle, bodyStyle, horizontalGridCells, + destroyInactiveTab, } = props; const visibleTabs = tabs.filter((tab) => !tab.hidden); @@ -242,7 +244,8 @@ const TabbedContainer = (props: TabbedContainerProps) => { return { label, key: tab.key, - forceRender: true, + forceRender: !destroyInactiveTab, + destroyInactiveTabPane: destroyInactiveTab, children: ( @@ -315,8 +318,9 @@ export const TabbedContainerBaseComp = (function () {
{children.onEvent.getPropertyView()} {disabledPropertyView(children)} - {children.showHeader.propertyView({ label: trans("tabbedContainer.showTabs") })} {hiddenPropertyView(children)} + {children.showHeader.propertyView({ label: trans("tabbedContainer.showTabs") })} + {children.destroyInactiveTab.propertyView({ label: trans("tabbedContainer.destroyInactiveTab") })}
)} diff --git a/client/packages/lowcoder/src/comps/comps/textComp.tsx b/client/packages/lowcoder/src/comps/comps/textComp.tsx index 1dd20cae8..38043d1a4 100644 --- a/client/packages/lowcoder/src/comps/comps/textComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textComp.tsx @@ -24,7 +24,7 @@ import { clickEvent, eventHandlerControl } from "../controls/eventHandlerControl import { NewChildren } from "../generators/uiCompBuilder"; import { RecordConstructorToComp } from "lowcoder-core"; import { ToViewReturn } from "../generators/multi"; -import { BoolControl } from "@lowcoder-ee/index.sdk"; +import { BoolControl } from "../controls/boolControl"; const EventOptions = [clickEvent] as const; diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx index dda409de1..cdfb952b6 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx @@ -28,7 +28,7 @@ import { SelectEventHandlerControl } from "comps/controls/eventHandlerControl"; import { trans } from "i18n"; import { useContext } from "react"; import { EditorContext } from "comps/editorState"; -import { AutoHeightControl } from "@lowcoder-ee/index.sdk"; +import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl"; type TreeStyleType = StyleConfigType; diff --git a/client/packages/lowcoder/src/comps/controls/iconControl.tsx b/client/packages/lowcoder/src/comps/controls/iconControl.tsx index 2df937259..767000ad8 100644 --- a/client/packages/lowcoder/src/comps/controls/iconControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconControl.tsx @@ -26,11 +26,12 @@ import { useIcon, wrapperToControlItem, } from "lowcoder-design"; -import { ReactNode, useCallback, useState } from "react"; +import { memo, ReactNode, useCallback, useMemo, useState } from "react"; import styled from "styled-components"; import { setFieldsNoTypeCheck } from "util/objectUtils"; import { StringControl } from "./codeControl"; import { ControlParams } from "./controlParams"; +import { IconDictionary } from "@lowcoder-ee/constants/iconConstants"; const ButtonWrapper = styled.div` width: 100%; @@ -208,14 +209,24 @@ type ChangeModeAction = { useCodeEditor: boolean; }; -export function IconControlView(props: { value: string }) { +export const IconControlView = memo((props: { value: string }) => { const { value } = props; const icon = useIcon(value); - if (icon) { - return icon.getView(); - } - return ; -} + + return useMemo(() => { + if (value && IconDictionary[value] && IconDictionary[value]?.title === icon?.title) { + return IconDictionary[value]; + } + + if (value && icon) { + const renderIcon = icon.getView(); + IconDictionary[value] = renderIcon; + return renderIcon; + } + + return ; + }, [icon, value, IconDictionary[value]]) +}); export class IconControl extends AbstractComp>> { private readonly useCodeEditor: boolean; diff --git a/client/packages/lowcoder/src/comps/generators/withMultiContext.tsx b/client/packages/lowcoder/src/comps/generators/withMultiContext.tsx index 380e8744b..41088108b 100644 --- a/client/packages/lowcoder/src/comps/generators/withMultiContext.tsx +++ b/client/packages/lowcoder/src/comps/generators/withMultiContext.tsx @@ -124,11 +124,14 @@ export function withMultiContext(VariantComp const mapComps = this.getMap(); if (mapComps.hasOwnProperty(key) && !paramsEqual(params, mapComps[key].getParams())) { // refresh the item, since params changed - this.dispatch(deferAction(wrapChildAction(MAP_KEY, MapCtor.batchDeleteAction([key])))); + // this.dispatch(deferAction(wrapChildAction(MAP_KEY, MapCtor.batchDeleteAction([key])))); + this.dispatch(wrapChildAction(MAP_KEY, MapCtor.batchDeleteAction([key]))); + comp = this.getOriginalComp(); + } else { + comp = this.getOriginalComp() + .setParams(params) + .changeDispatch(wrapDispatch(wrapDispatch(this.dispatch, MAP_KEY), key)); } - comp = this.getOriginalComp() - .setParams(params) - .changeDispatch(wrapDispatch(wrapDispatch(this.dispatch, MAP_KEY), key)); } return comp; } diff --git a/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx b/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx index 6c04fa8aa..9d52075e9 100644 --- a/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx @@ -45,9 +45,19 @@ const StyledDrawer = styled(Drawer)<{$titleAlign?: string, $drawerScrollbar: boo .ant-drawer-header-title { margin: 0px 20px !important; text-align: ${(props) => props.$titleAlign || "center"}; + + .ant-drawer-title { + position: relative; + z-index: 11; + } } - div.ant-drawer-body div.react-grid-layout::-webkit-scrollbar { - display: ${(props) => props.$drawerScrollbar ? "block" : "none"}; + + div.ant-drawer-body div.react-grid-layout { + overflow: auto; + + &::-webkit-scrollbar { + display: ${(props) => props.$drawerScrollbar ? "block" : "none"}; + } } `; @@ -102,7 +112,8 @@ let TmpDrawerComp = (function () { closePosition: withDefault(LeftRightControl, "left"), maskClosable: withDefault(BoolControl, true), showMask: withDefault(BoolControl, true), - toggleClose:withDefault(BoolControl,true) + toggleClose:withDefault(BoolControl,true), + escapeClosable: withDefault(BoolControl, true), }, (props, dispatch) => { const isTopBom = ["top", "bottom"].includes(props.placement); @@ -143,6 +154,7 @@ let TmpDrawerComp = (function () { $titleAlign={props.titleAlign} $drawerScrollbar={props.drawerScrollbar} closable={false} + keyboard={props.escapeClosable} placement={props.placement} open={props.visible.value} getContainer={() => document.querySelector(`#${CanvasContainerID}`) || document.body} @@ -224,6 +236,9 @@ let TmpDrawerComp = (function () { {children.toggleClose.propertyView({ label: trans("prop.toggleClose"), })} + {children.escapeClosable.propertyView({ + label: trans("prop.escapeClose"), + })}
{children.onEvent.getPropertyView()}
{children.style.getPropertyView()}
diff --git a/client/packages/lowcoder/src/comps/utils/remote.ts b/client/packages/lowcoder/src/comps/utils/remote.ts index c0984d5d0..e012ed084 100644 --- a/client/packages/lowcoder/src/comps/utils/remote.ts +++ b/client/packages/lowcoder/src/comps/utils/remote.ts @@ -39,7 +39,13 @@ export function parseCompType(compType: string) { } export async function getNpmPackageMeta(packageName: string) { - const res = await axios.get(`${NPM_REGISTRY_URL}/none/${packageName}`); + const axiosInstance = axios.create({ + baseURL: NPM_REGISTRY_URL, + withCredentials: true, + }) + const res = await axiosInstance.get( + `/none/${packageName}`, + ); if (res.status >= 400) { return null; } diff --git a/client/packages/lowcoder/src/comps/utils/supademoDisplay.tsx b/client/packages/lowcoder/src/comps/utils/supademoDisplay.tsx index 1c1afb4d4..c29059827 100644 --- a/client/packages/lowcoder/src/comps/utils/supademoDisplay.tsx +++ b/client/packages/lowcoder/src/comps/utils/supademoDisplay.tsx @@ -35,7 +35,7 @@ const SupaDemoDisplay = ({ url, modalWidth = '75%', modalTop = '6%', showText = style={{ top: modalTop }} okButtonProps={{ style: { display: 'none' } }} cancelButtonProps={{ style: { display: 'none' } }} - bodyStyle={{ padding: 0 }} + styles={{ body: {padding: 0} }} >