From f4f8fc3334cb968fb2b18fa5b2aa006338b8964a Mon Sep 17 00:00:00 2001
From: triminhvnt <triminhvnt@gmail.com>
Date: Fri, 20 Jun 2025 17:01:45 +0700
Subject: [PATCH 1/7] initial commit

---
 .../webpack.config.cjs                        |   3 +-
 client/packages/lowcoder-sdk/index.tsx        |   3 +-
 client/packages/lowcoder-sdk/vite.config.mts  |   3 +-
 client/packages/lowcoder/src/api/configApi.ts |   1 +
 .../lowcoder/src/constants/configConstants.ts |  13 ++
 .../src/pages/userAuth/formLoginAdmin.tsx     |   7 +-
 .../src/pages/userAuth/formLoginSteps.tsx     |  16 ++-
 .../lowcoder/src/pages/userAuth/login.tsx     |   6 +-
 .../lowcoder/src/util/pagination/axios.ts     |   2 +
 .../service/ServerSettingServiceImpl.java     |  28 +++-
 .../main/resources/application-lowcoder.yaml  | 135 ++++++++++++++++++
 11 files changed, 201 insertions(+), 16 deletions(-)
 create mode 100644 server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yaml

diff --git a/client/packages/lowcoder-sdk-webpack-bundle/webpack.config.cjs b/client/packages/lowcoder-sdk-webpack-bundle/webpack.config.cjs
index 02efbc1572..a5be4cc8b3 100644
--- a/client/packages/lowcoder-sdk-webpack-bundle/webpack.config.cjs
+++ b/client/packages/lowcoder-sdk-webpack-bundle/webpack.config.cjs
@@ -14,7 +14,8 @@ buildVars.forEach(({ name, defaultValue }) => {
   define[name] = JSON.stringify(process.env[name] || defaultValue);
 });
 
-const apiBaseUrl = "http://localhost:8000";
+//const apiBaseUrl = "http://localhost:8000";
+const apiBaseUrl = "http://localhost:8080";
 
 module.exports = {
   mode: 'production',
diff --git a/client/packages/lowcoder-sdk/index.tsx b/client/packages/lowcoder-sdk/index.tsx
index 54fc1bb97f..7c700089e7 100644
--- a/client/packages/lowcoder-sdk/index.tsx
+++ b/client/packages/lowcoder-sdk/index.tsx
@@ -5,7 +5,8 @@ import { AppViewInstance, bootstrapAppAt, LowcoderAppView } from "./src/index";
 
 const url = new URL(location.href);
 const appId = url.searchParams.get("appId");
-const baseUrl = url.searchParams.get("baseUrl") || "http://localhost:8000";
+//const baseUrl = url.searchParams.get("baseUrl") || "http://localhost:8000";
+const baseUrl = url.searchParams.get("baseUrl") || "http://localhost:8080";
 
 function ReactDemoApp() {
   const ref = useRef<AppViewInstance | null>(null);
diff --git a/client/packages/lowcoder-sdk/vite.config.mts b/client/packages/lowcoder-sdk/vite.config.mts
index 3cd7b5c454..8169957ebd 100644
--- a/client/packages/lowcoder-sdk/vite.config.mts
+++ b/client/packages/lowcoder-sdk/vite.config.mts
@@ -18,7 +18,8 @@ buildVars.forEach(({ name, defaultValue }) => {
   define[name] = JSON.stringify(process.env[name] || defaultValue);
 });
 
-const apiBaseUrl = "http://localhost:8000";
+//const apiBaseUrl = "http://localhost:8000";
+const apiBaseUrl = "http://localhost:8080";
 
 export const viteConfig: UserConfig = {
   define: {
diff --git a/client/packages/lowcoder/src/api/configApi.ts b/client/packages/lowcoder/src/api/configApi.ts
index fc756069ed..bb36df0ef3 100644
--- a/client/packages/lowcoder/src/api/configApi.ts
+++ b/client/packages/lowcoder/src/api/configApi.ts
@@ -15,6 +15,7 @@ class ConfigApi extends Api {
     if(orgId?.length) {
       authConfigURL += `?orgId=${orgId}`;
     }
+    console.log('authConfigURL:', authConfigURL);
     return Api.get(authConfigURL);
   }
 
diff --git a/client/packages/lowcoder/src/constants/configConstants.ts b/client/packages/lowcoder/src/constants/configConstants.ts
index 1a0077009a..a195fcff5d 100644
--- a/client/packages/lowcoder/src/constants/configConstants.ts
+++ b/client/packages/lowcoder/src/constants/configConstants.ts
@@ -71,6 +71,7 @@ export type SystemConfig = {
   authConfigs: ThirdPartyConfigType[];
 } & ConfigBaseInfo;
 
+// xu ly du lieu cau hinh tra ve tu server
 export const transToSystemConfig = (responseData: ConfigResponseData): SystemConfig => {
   const thirdPartyAuthConfigs: ThirdPartyConfigType[] = [];
   responseData.authConfigs?.forEach((authConfig) => {
@@ -106,6 +107,18 @@ export const transToSystemConfig = (responseData: ConfigResponseData): SystemCon
     (c) => c.source === UserConnectionSource.email
   ) as FormConfig | undefined;
 
+  const tmp = {
+    ...responseData,
+    form: {
+      enableRegister: !!emailConfig?.enableRegister,
+      enableLogin: !!emailConfig?.enable,
+      id: emailConfig?.id,
+      type: "EMAIL",
+    },
+    authConfigs: thirdPartyAuthConfigs,
+  };
+  console.log('AuthContext login:', tmp);
+
   return {
     ...responseData,
     form: {
diff --git a/client/packages/lowcoder/src/pages/userAuth/formLoginAdmin.tsx b/client/packages/lowcoder/src/pages/userAuth/formLoginAdmin.tsx
index 16cd72ad39..4305801dae 100644
--- a/client/packages/lowcoder/src/pages/userAuth/formLoginAdmin.tsx
+++ b/client/packages/lowcoder/src/pages/userAuth/formLoginAdmin.tsx
@@ -39,14 +39,15 @@ export default function FormLogin(props: FormLoginProps) {
   }
 
   const { onSubmit, loading } = useAuthSubmit(
-    () =>
-      UserApi.formLogin({
+    () => {
+      return UserApi.formLogin({
         register: false,
         loginId: account,
         password: password,
         source: UserConnectionSource.email,
         orgId: props.organizationId,
-      }),
+      });
+    },
     false,
     null,
     afterLoginSuccess,
diff --git a/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
index 9b88b019cf..3ab3874888 100644
--- a/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
+++ b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
@@ -124,10 +124,12 @@ export default function FormLoginSteps(props: FormLoginProps) {
   const [currentPage, setCurrentPage] = useState(1);
   const [pageSize, setPageSize] = useState(10);
 
-  const isEmailLoginEnabled = useMemo(() => {
-    return isFormLoginEnabled && signinEnabled;
-  }, [isFormLoginEnabled, signinEnabled]);
+  // const isEmailLoginEnabled = useMemo(() => {
+  //   return isFormLoginEnabled && signinEnabled;
+  // }, [isFormLoginEnabled, signinEnabled]);
   
+  const isEmailLoginEnabled = true;
+
   const isEnterpriseMode = useMemo(() => {
     return serverSettings?.LOWCODER_WORKSPACE_MODE === "ENTERPRISE" || serverSettings?.LOWCODER_WORKSPACE_MODE === "SINGLEWORKSPACE";
   }, [serverSettings]);
@@ -164,8 +166,8 @@ export default function FormLoginSteps(props: FormLoginProps) {
   }
 
   const { onSubmit, loading } = useAuthSubmit(
-    () =>
-      UserApi.formLogin({
+    () => {
+      return UserApi.formLogin({
         register: false,
         loginId: account,
         password: password,
@@ -173,7 +175,8 @@ export default function FormLoginSteps(props: FormLoginProps) {
         source: UserConnectionSource.email,
         orgId: organizationId,
         authId,
-      }),
+      });
+    },
     false,
     redirectUrl,
     afterLoginSuccess,
@@ -260,7 +263,6 @@ export default function FormLoginSteps(props: FormLoginProps) {
       </Spin>
     );
   }
-
   if(currentStep === CurrentStepEnum.EMAIL) {
     return (
       <>
diff --git a/client/packages/lowcoder/src/pages/userAuth/login.tsx b/client/packages/lowcoder/src/pages/userAuth/login.tsx
index acc36e234e..0499e5f13b 100644
--- a/client/packages/lowcoder/src/pages/userAuth/login.tsx
+++ b/client/packages/lowcoder/src/pages/userAuth/login.tsx
@@ -61,7 +61,7 @@ const ThirdAuthWrapper = styled.div`
   }
 `;
 
-// this is used to bind multiple third party logins to an sngle account
+// this is used to bind multiple third party logins to an single account
 const thirdPartyLoginLabel = (name: string) => trans("userAuth.signInLabel", { name: name });
 
 export const ThirdPartyBindCard = () => {
@@ -92,9 +92,13 @@ function Login() {
   const queryParams = new URLSearchParams(location.search);
   const { orgId } = useParams<{orgId?: string}>();
 
+  const tmp1 = useContext(AuthContext);
+  console.log('AuthContext after login:', tmp1);
+  
   const loginType = systemConfig?.authConfigs.find(
     (config) => config.sourceType === queryParams.get(AuthSearchParams.loginType)
   )?.sourceType;
+  
   let autoJumpSource: string | undefined;
   if (loginType) {
     autoJumpSource = loginType;
diff --git a/client/packages/lowcoder/src/util/pagination/axios.ts b/client/packages/lowcoder/src/util/pagination/axios.ts
index d03bf1800c..dc64183b3d 100644
--- a/client/packages/lowcoder/src/util/pagination/axios.ts
+++ b/client/packages/lowcoder/src/util/pagination/axios.ts
@@ -162,7 +162,9 @@ export const fetchJsDSPaginationByApp = async (request: fetchDataSourcePaginatio
 
 export const fetchOrgPaginationByEmail = async (request: fetchOrgsByEmailRequestType)=> {
     try {
+        console.log('fetchOrgPaginationByEmail:', request);
         const response = await OrgApi.fetchOrgsPaginationByEmail(request);
+        console.log('fetchOrgPaginationByEmail response:', response);
         return {
             success: true,
             data: response.data.data,
diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java
index 64e1847ab6..cce0b07666 100644
--- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java
+++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java
@@ -62,7 +62,7 @@ public void saveEnvironmentVariables() {
         Flux.fromIterable(merged.keySet())
                 .map(key -> {
                     String value = envVariables.getOrDefault(key, "");
-                    if(EXCLUDED_KEYS.contains(key)) {
+                    if (EXCLUDED_KEYS.contains(key)) {
                         value = "stored on the server";
                     }
                     return ServerSetting.builder()
@@ -70,8 +70,32 @@ public void saveEnvironmentVariables() {
                             .value(value)
                             .build();
                 })
-                .flatMap(repository::save)
+                .flatMap(setting ->
+                        repository.findById(setting.getKey())
+                                .flatMap(existing -> {
+                                    if (!Objects.equals(existing.getValue(), setting.getValue())) {
+                                        return repository.save(setting); // giá trị khác → cập nhật
+                                    } else {
+                                        return Mono.empty(); // giống nhau → bỏ qua
+                                    }
+                                })
+                                .switchIfEmpty(repository.save(setting)) // chưa có → thêm mới
+                )
                 .subscribe();
+        // tạm che đoạn code không kiểm tra trùng key
+//        Flux.fromIterable(merged.keySet())
+//                .map(key -> {
+//                    String value = envVariables.getOrDefault(key, "");
+//                    if(EXCLUDED_KEYS.contains(key)) {
+//                        value = "stored on the server";
+//                    }
+//                    return ServerSetting.builder()
+//                            .key(key)
+//                            .value(value)
+//                            .build();
+//                })
+//                .flatMap(repository::save)
+//                .subscribe();
     }
 
 
diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yaml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yaml
new file mode 100644
index 0000000000..0b323b7b43
--- /dev/null
+++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yaml
@@ -0,0 +1,135 @@
+auth:
+  api-key:
+    secret: ${LOWCODER_API_KEY_SECRET:5a41b090758b39b226603177ef48d73ae9839dd458ccb7e66f7e7cc028d5a50b}
+  email:
+    enable: ${LOWCODER_EMAIL_AUTH_ENABLED:true}
+    enable-register: ${LOWCODER_EMAIL_SIGNUP_ENABLED:true}
+  workspace-creation: ${LOWCODER_CREATE_WORKSPACE_ON_SIGNUP:true}
+
+spring:
+  data:
+    mongodb:
+      authentication-database: admin
+      auto-index-creation: false
+      uri: ${LOWCODER_MONGODB_URL:mongodb://lowcoder:secret123@localhost:27017/lowcoder?retryWrites=true&loadBalanced=false&connectTimeoutMS=10000&authSource=admin&authMechanism=SCRAM-SHA-256}
+    redis:
+      url: ${LOWCODER_REDIS_URL:redis://localhost:6379}
+  main:
+    allow-bean-definition-overriding: false
+    allow-circular-references: false
+  codec:
+    max-in-memory-size: 20MB
+  webflux:
+    base-path: /
+  mail:
+    host: ${LOWCODER_ADMIN_SMTP_HOST:localhost}
+    port: ${LOWCODER_ADMIN_SMTP_PORT:587}
+    username: ${LOWCODER_ADMIN_SMTP_USERNAME:info@localhost}
+    password: ${LOWCODER_ADMIN_SMTP_PASSWORD:s3cr3t}
+    properties:
+      mail:
+        smtp:
+          auth: ${LOWCODER_ADMIN_SMTP_AUTH:true}
+          ssl:
+            enable: ${LOWCODER_ADMIN_SMTP_SSL_ENABLED:false}
+          starttls:
+            enable: ${LOWCODER_ADMIN_SMTP_STARTTLS_ENABLED:true}
+            required: ${LOWCODER_ADMIN_SMTP_STARTTLS_REQUIRED:true}
+        transport:
+          protocol: smtp
+  mvc:
+    pathmatch:
+      trailing-slash-match: true
+server:
+  compression:
+    enabled: true
+  forward-headers-strategy: NATIVE
+  http2:
+    enabled: true
+  port: 8080
+  shutdown: graceful
+
+default:
+  orgs-per-user: ${LOWCODER_MAX_ORGS_PER_USER:100}
+  org-member-count: ${LOWCODER_MAX_MEMBERS_PER_ORG:1000}
+  org-group-count: ${LOWCODER_MAX_GROUPS_PER_ORG:100}
+  org-app-count: ${LOWCODER_MAX_APPS_PER_ORG:1000}
+  developer-count: ${LOWCODER_MAX_DEVELOPERS:50}
+  api-rate-limit: ${LOWCODER_API_RATE_LIMIT:50}
+  query-timeout: ${LOWCODER_DEFAULT_QUERY_TIMEOUT:10}
+
+common:
+  cookie-name: ${LOWCODER_COOKIE_NAME:LOWCODER_CE_SELFHOST_TOKEN}
+  product: lowcoder
+  domain:
+    default-value: lowcoder.org
+  cloud: false
+  version: 2.7.2
+  apiVersion: 1.2
+  block-hound-enable: false
+  encrypt:
+    password: ${LOWCODER_DB_ENCRYPTION_PASSWORD:lowcoder.org}
+    salt: ${LOWCODER_DB_ENCRYPTION_SALT:lowcoder.org}
+  security:
+    corsAllowedDomainString: ${LOWCODER_CORS_DOMAINS:*}
+  js-executor:
+    host: ${LOWCODER_NODE_SERVICE_URL:http://127.0.0.1:6060}
+    password: ${LOWCODER_NODE_SERVICE_SECRET:}
+    salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:}
+  max-query-request-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
+  max-query-response-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
+  max-upload-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
+  max-query-timeout: ${LOWCODER_MAX_QUERY_TIMEOUT:120}
+  workspace:
+    mode: ${LOWCODER_WORKSPACE_MODE:SAAS}
+  plugin-dirs:
+    - ${LOWCODER_PLUGINS_DIR:../plugins}
+  super-admin:
+    username: ${LOWCODER_SUPERUSER_USERNAME:admin@localhost}
+    password: ${LOWCODER_SUPERUSER_PASSWORD:secret123}
+  marketplace:
+    private-mode: ${LOWCODER_MARKETPLACE_PRIVATE_MODE:true}
+  lowcoder-public-url: ${LOWCODER_PUBLIC_URL:http://localhost:3000}
+  notifications-email-sender: ${LOWCODER_EMAIL_NOTIFICATIONS_SENDER:info@localhost}
+  cookie:
+    max-age-in-hours: ${LOWCODER_COOKIE_MAX_AGE:24}
+  query:
+    app-snapshot-keep-duration: ${LOWCODER_APP_SNAPSHOT_RETENTIONTIME:30}
+
+material:
+  mongodb-grid-fs:
+    bucket-name: material
+
+springdoc:
+  api-docs:
+    path: /api/docs/openapi.json
+  swagger-ui:
+    path: /api/docs/swagger-ui
+    operations-sorter: method
+  paths-to-exclude: /api/v1/**
+
+management:
+  endpoints:
+    enabled-by-default: false
+    web:
+      base-path: "/api/status"
+      exposure:
+        include: "health,metrics,prometheus"
+  endpoint:
+    health:
+      show-details: never
+      show-components: always
+      enabled: true
+    metrics:
+      enabled: true
+    prometheus:
+      enabled: true
+  health:
+    mail:
+      enabled: false
+    db:
+      enabled: true
+    redis:
+      enabled: true
+    diskspace:
+      enabled: false
\ No newline at end of file

From 8eeb0921024b50ac05102280b1def7e622e002ba Mon Sep 17 00:00:00 2001
From: triminhvnt <triminhvnt@gmail.com>
Date: Mon, 23 Jun 2025 16:44:27 +0700
Subject: [PATCH 2/7] rollback and add docker yml

---
 .../src/pages/userAuth/formLoginSteps.tsx     |   8 +-
 server/api-service/docker-compose-multi.yaml  | 153 ++++++++++++++++++
 server/api-service/docker-compose.yml         |  46 ++++++
 .../service/ServerSettingServiceImpl.java     |  27 +---
 4 files changed, 204 insertions(+), 30 deletions(-)
 create mode 100644 server/api-service/docker-compose-multi.yaml
 create mode 100644 server/api-service/docker-compose.yml

diff --git a/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
index 3ab3874888..5df243645d 100644
--- a/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
+++ b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
@@ -124,11 +124,11 @@ export default function FormLoginSteps(props: FormLoginProps) {
   const [currentPage, setCurrentPage] = useState(1);
   const [pageSize, setPageSize] = useState(10);
 
-  // const isEmailLoginEnabled = useMemo(() => {
-  //   return isFormLoginEnabled && signinEnabled;
-  // }, [isFormLoginEnabled, signinEnabled]);
+  const isEmailLoginEnabled = useMemo(() => {
+    return isFormLoginEnabled && signinEnabled;
+  }, [isFormLoginEnabled, signinEnabled]);
   
-  const isEmailLoginEnabled = true;
+  //const isEmailLoginEnabled = true;
 
   const isEnterpriseMode = useMemo(() => {
     return serverSettings?.LOWCODER_WORKSPACE_MODE === "ENTERPRISE" || serverSettings?.LOWCODER_WORKSPACE_MODE === "SINGLEWORKSPACE";
diff --git a/server/api-service/docker-compose-multi.yaml b/server/api-service/docker-compose-multi.yaml
new file mode 100644
index 0000000000..70833c33b2
--- /dev/null
+++ b/server/api-service/docker-compose-multi.yaml
@@ -0,0 +1,153 @@
+services:
+  mongodb:
+    image: "mongo:7.0"
+    container_name: mongodb
+    ports:
+      - "27017:27017"
+    environment:
+      MONGO_INITDB_DATABASE: lowcoder
+      MONGO_INITDB_ROOT_USERNAME: lowcoder
+      MONGO_INITDB_ROOT_PASSWORD: secret123
+    volumes:
+      - ./lowcoder-stacks/data/mongodb:/data/db
+    restart: unless-stopped
+    networks:
+      - lowcoder-network
+    healthcheck: # https://github.com/rodrigobdz/docker-compose-healthchecks?tab=readme-ov-file#mongo
+      test:
+        [
+          "CMD",
+          "mongosh",
+          "--quiet",
+          "127.0.0.1/test",
+          "--eval",
+          "'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)'",
+        ]
+      interval: 5s
+      timeout: 10s
+      retries: 10
+      start_period: 40s
+  
+  postgres:
+    image: postgres:16
+    container_name: postgres
+    environment:
+      - POSTGRES_DB=picodb
+      - POSTGRES_USER=picowood
+      - POSTGRES_PASSWORD=picowood123
+    ports:
+      - "5432:5432"
+    volumes:
+      - postgres-data:/var/lib/postgresql/data
+    restart: unless-stopped
+    networks:
+      - lowcoder-network
+    
+  redis:
+    image: redis:7-alpine
+    container_name: redis
+    ports:
+      - "6379:6379"
+    restart: unless-stopped
+    networks:
+      - lowcoder-network
+    healthcheck: # https://stackoverflow.com/a/71504657
+      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
+      interval: 1s
+      timeout: 3s
+      retries: 10
+
+  lowcoder-api-service:
+    image: lowcoderorg/lowcoder-ce-api-service:latest
+    container_name: lowcoder-api-service
+    ports:
+      - "8080:8080"
+    env_file:
+      - path: ./default.env
+        required: true
+      - path: ./default-multi.env
+        required: true
+      - path: ./override.env
+        required: false
+    restart: unless-stopped
+    networks:
+      - lowcoder-network
+    depends_on:
+      mongodb:
+        condition: service_healthy
+        restart: true
+      redis:
+        condition: service_healthy
+        restart: true
+    volumes:
+      - ./lowcoder-stacks:/lowcoder-stacks
+      - ./lowcoder-stacks/assets:/lowcoder/assets
+    healthcheck: #https://stackoverflow.com/questions/71101967/how-should-i-use-grep-in-docker-compose-healthcheck
+      test: curl -sS http://lowcoder-api-service:8080  | grep -c "Lowcoder API is up and runnig" > /dev/null
+      interval: 3s
+      timeout: 5s
+      retries: 10
+
+  lowcoder-node-service:
+    image: lowcoderorg/lowcoder-ce-node-service:latest
+    container_name: lowcoder-node-service
+    ports:
+      - "6060:6060"
+    env_file:
+      - path: ./default.env
+        required: true
+      - path: ./default-multi.env
+        required: true
+      - path: ./override.env
+        required: false
+    restart: unless-stopped
+    networks:
+      - lowcoder-network
+    depends_on:
+      lowcoder-api-service:
+        condition: service_healthy
+        restart: true
+    healthcheck: #https://stackoverflow.com/questions/71101967/how-should-i-use-grep-in-docker-compose-healthcheck
+      test: curl -sS http://lowcoder-node-service:6060  | grep -c "Lowcoder Node Service is up and running" > /dev/null
+      interval: 3s
+      timeout: 5s
+      retries: 10
+
+  lowcoder-frontend:
+    image: lowcoderorg/lowcoder-ce-frontend:latest
+    container_name: lowcoder-frontend
+    ports:
+      - "3000:3000"
+    env_file:
+      - path: ./default.env
+        required: true
+      - path: ./default-multi.env
+        required: true
+      - path: ./override.env
+        required: false
+    restart: unless-stopped
+    networks:
+      - lowcoder-network
+    depends_on:
+      lowcoder-node-service:
+        condition: service_healthy
+        restart: true
+      lowcoder-api-service:
+        condition: service_healthy
+        restart: true
+    volumes:
+      - ./lowcoder-stacks/assets:/lowcoder/assets
+      - ./lowcoder-stacks/ssl:/lowcoder-stacks/ssl
+    healthcheck:
+      test: curl --fail http://lowcoder-frontend:3000 || exit 1
+      interval: 5s
+      retries: 10
+      start_period: 10s
+      timeout: 10s
+      
+volumes:
+  postgres-data:
+  
+networks:
+  lowcoder-network:
+    driver: bridge
\ No newline at end of file
diff --git a/server/api-service/docker-compose.yml b/server/api-service/docker-compose.yml
new file mode 100644
index 0000000000..d0191aee04
--- /dev/null
+++ b/server/api-service/docker-compose.yml
@@ -0,0 +1,46 @@
+version: '3.8'
+
+services:
+  lowcoder:
+    image: lowcoderorg/lowcoder-ce:latest
+    container_name: lowcoder
+    env_file:
+      - path: ./default.env
+        required: true
+      - path: ./override.env
+        required: false
+    ports:
+      - "3000:3000"
+      - "3443:3443"
+      - "27017:27017"
+    volumes:
+      - ./lowcoder-stacks:/lowcoder-stacks
+      - ./lowcoder-stacks/assets:/lowcoder/assets
+    depends_on:
+      - postgres
+    networks:
+      - lowcoder-network
+    restart: always
+    
+  postgres:
+    image: postgres:16
+    container_name: postgres
+    environment:
+      - POSTGRES_DB=lowcoder
+      - POSTGRES_USER=lowcoder_user
+      - POSTGRES_PASSWORD=580784d01662f1f7e84d8c0208a5cf29996ef1ab667c775d6c7877657bb5f28f
+    ports:
+      - "5432:5432"
+    volumes:
+      - postgres-data:/var/lib/postgresql/data
+    networks:
+      - lowcoder-network
+    restart: always
+    
+volumes:
+  lowcoder-stacks:
+  postgres-data:
+
+networks:
+  lowcoder-network:
+    driver: bridge
\ No newline at end of file
diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java
index cce0b07666..8eaae58c5c 100644
--- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java
+++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/serversetting/service/ServerSettingServiceImpl.java
@@ -70,35 +70,10 @@ public void saveEnvironmentVariables() {
                             .value(value)
                             .build();
                 })
-                .flatMap(setting ->
-                        repository.findById(setting.getKey())
-                                .flatMap(existing -> {
-                                    if (!Objects.equals(existing.getValue(), setting.getValue())) {
-                                        return repository.save(setting); // giá trị khác → cập nhật
-                                    } else {
-                                        return Mono.empty(); // giống nhau → bỏ qua
-                                    }
-                                })
-                                .switchIfEmpty(repository.save(setting)) // chưa có → thêm mới
-                )
+                .flatMap(repository::save)
                 .subscribe();
-        // tạm che đoạn code không kiểm tra trùng key
-//        Flux.fromIterable(merged.keySet())
-//                .map(key -> {
-//                    String value = envVariables.getOrDefault(key, "");
-//                    if(EXCLUDED_KEYS.contains(key)) {
-//                        value = "stored on the server";
-//                    }
-//                    return ServerSetting.builder()
-//                            .key(key)
-//                            .value(value)
-//                            .build();
-//                })
-//                .flatMap(repository::save)
-//                .subscribe();
     }
 
-
     private Map<String, String> getEnvironmentVariablesDefaults() {
         Map<String, String> defaults = new HashMap<>();
 

From c14a352c13d854aa2496820e1ac2b865e19400b3 Mon Sep 17 00:00:00 2001
From: triminhvnt <triminhvnt@gmail.com>
Date: Fri, 27 Jun 2025 17:07:29 +0700
Subject: [PATCH 3/7] =?UTF-8?q?b=E1=BB=95=20sung=20suffix=20icon?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../tableComp/column/columnTypeComps/columnLinkComp.tsx  | 3 ++-
 .../tableComp/column/columnTypeComps/columnLinksComp.tsx | 6 +++++-
 .../lowcoder/src/pages/userAuth/formLoginSteps.tsx       | 9 +++++----
 3 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinkComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinkComp.tsx
index e93b3082a6..adda461edb 100644
--- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinkComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinkComp.tsx
@@ -39,7 +39,8 @@ const StyledLink = styled.a<{ $disabled: boolean }>`
 `;
 
 // Memoized link component
-export const ColumnLink = React.memo(({ disabled, label, onClick }: { disabled: boolean; label: string; onClick: (eventName: string) => void }) => {
+// export const ColumnLink = React.memo(({ disabled, label, onClick }: { disabled: boolean; label: string; onClick: (eventName: string) => void }) => {
+export const ColumnLink = React.memo(({ disabled, label, onClick }: { disabled: boolean; label: React.ReactNode; onClick: (eventName: string) => void }) => {
   const handleClickEvent = useCompClickEventHandler({onEvent: onClick})
   const handleClick = useCallback(() => {
     if (!disabled) {
diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx
index 5a7fae3d3e..5deb107e61 100644
--- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx
@@ -13,6 +13,7 @@ import { clickEvent, eventHandlerControl, doubleClickEvent } from "comps/control
 import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler";
 import { migrateOldData } from "@lowcoder-ee/comps/generators/simpleGenerators";
 import { fixOldActionData } from "comps/comps/tableComp/column/simpleColumnTypeComps";
+import { IconControl } from "comps/controls/iconControl"; // thêm thư viện icon
 
 const MenuLinkWrapper = styled.div`
   > a {
@@ -48,7 +49,8 @@ const MenuItem = React.memo(({ option, index }: { option: any; index: number })
     <MenuLinkWrapper>
       <ColumnLink
         disabled={option.disabled}
-        label={option.label}
+        // label={option.label}
+        label={<><span style={{marginRight: option.prefixIcon ? 4 : 0}}>{option.prefixIcon}</span>{option.label}</>} // bổ sung điều chỉnh prefix icon cho links
         onClick={option.onClick}
       />
     </MenuLinkWrapper>
@@ -60,6 +62,7 @@ MenuItem.displayName = 'MenuItem';
 const OptionItemTmp = new MultiCompBuilder(
   {
     label: StringControl,
+    prefixIcon: IconControl, // thêm prefix icon
     onClick: eventHandlerControl(LinkEventOptions),
     hidden: BoolCodeControl,
     disabled: BoolCodeControl,
@@ -72,6 +75,7 @@ const OptionItemTmp = new MultiCompBuilder(
     return (
       <>
         {children.label.propertyView({ label: trans("label") })}
+        {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} 
         {hiddenPropertyView(children)}
         {disabledPropertyView(children)}
         {children.onClick.propertyView()}
diff --git a/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
index 5df243645d..67236d6774 100644
--- a/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
+++ b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
@@ -124,11 +124,12 @@ export default function FormLoginSteps(props: FormLoginProps) {
   const [currentPage, setCurrentPage] = useState(1);
   const [pageSize, setPageSize] = useState(10);
 
-  const isEmailLoginEnabled = useMemo(() => {
-    return isFormLoginEnabled && signinEnabled;
-  }, [isFormLoginEnabled, signinEnabled]);
+  // Tạm che để cho phép đăng nhập với email
+  // const isEmailLoginEnabled = useMemo(() => {
+  //   return isFormLoginEnabled && signinEnabled;
+  // }, [isFormLoginEnabled, signinEnabled]);
   
-  //const isEmailLoginEnabled = true;
+  const isEmailLoginEnabled = true;
 
   const isEnterpriseMode = useMemo(() => {
     return serverSettings?.LOWCODER_WORKSPACE_MODE === "ENTERPRISE" || serverSettings?.LOWCODER_WORKSPACE_MODE === "SINGLEWORKSPACE";

From 31f60d3cc2ff3e61f0cc9edf4de39e14c108daf9 Mon Sep 17 00:00:00 2001
From: triminhvnt <triminhvnt@gmail.com>
Date: Wed, 2 Jul 2025 15:16:41 +0700
Subject: [PATCH 4/7] =?UTF-8?q?-=20fix=20l=E1=BB=97i=20kh=C3=B4ng=20load?=
 =?UTF-8?q?=20=C4=91=C6=B0=E1=BB=A3c=20event=20cho=20operation=20links=20-?=
 =?UTF-8?q?=20b=E1=BB=95=20sung=20suffix=20icon,=20prefix=20icon,=20text?=
 =?UTF-8?q?=20color=20cho=20links=20-=20th=C3=AAm=20m=E1=BB=9Bi=20trans?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../column/columnTypeComps/columnLinksComp.tsx    | 15 ++++++++++++---
 .../tableComp/column/simpleColumnTypeComps.tsx    |  8 ++++++--
 client/packages/lowcoder/src/i18n/locales/en.ts   |  1 +
 3 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx
index 5deb107e61..6f0a2f033f 100644
--- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx
@@ -49,8 +49,13 @@ const MenuItem = React.memo(({ option, index }: { option: any; index: number })
     <MenuLinkWrapper>
       <ColumnLink
         disabled={option.disabled}
-        // label={option.label}
-        label={<><span style={{marginRight: option.prefixIcon ? 4 : 0}}>{option.prefixIcon}</span>{option.label}</>} // bổ sung điều chỉnh prefix icon cho links
+        label={
+          <span style={{ color: option.color || undefined }}>
+            {option.prefixIcon && <span style={{marginRight: 4}}>{option.prefixIcon}</span>}
+            {option.label}
+            {option.suffixIcon && <span style={{marginLeft: 4}}>{option.suffixIcon}</span>}
+          </span>
+        }
         onClick={option.onClick}
       />
     </MenuLinkWrapper>
@@ -63,6 +68,8 @@ const OptionItemTmp = new MultiCompBuilder(
   {
     label: StringControl,
     prefixIcon: IconControl, // thêm prefix icon
+    suffixIcon: IconControl, // thêm suffix icon
+    color: StringControl, // thêm trường chỉnh màu chữ
     onClick: eventHandlerControl(LinkEventOptions),
     hidden: BoolCodeControl,
     disabled: BoolCodeControl,
@@ -76,6 +83,8 @@ const OptionItemTmp = new MultiCompBuilder(
       <>
         {children.label.propertyView({ label: trans("label") })}
         {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} 
+        {children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })} 
+        {children.color.propertyView({ label: trans("button.textColor") })}
         {hiddenPropertyView(children)}
         {disabledPropertyView(children)}
         {children.onClick.propertyView()}
@@ -83,7 +92,7 @@ const OptionItemTmp = new MultiCompBuilder(
     );
   })
   .build();
-
+// hàm chuyển đổi dữ liệu cấu hình cũ đã có(fixOldActionData) tương thích với cấu hình OptionItem mới(OptionItemTmp) và lưu thành OptionItem
 const OptionItem = migrateOldData(OptionItemTmp, fixOldActionData);
 
 // Memoized menu component
diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx
index 8ec51c6a1a..2e39f9e00b 100644
--- a/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx
+++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx
@@ -18,13 +18,17 @@ import { migrateOldData } from "@lowcoder-ee/comps/generators/simpleGenerators";
 import { useCompClickEventHandler } from "@lowcoder-ee/comps/utils/useCompClickEventHandler";
 
 export const fixOldActionData = (oldData: any) => {
-  if (!oldData) return oldData;
+  // check if oldData is null
+  if (!oldData) { 
+    return oldData; 
+  }
   if (Boolean(oldData.onClick)) {
     return {
       ...oldData,
       onClick: [{
         name: "click",
-        handler: oldData.onClick,
+        // handler: oldData.onClick,
+        handler: oldData.onClick[0].handler, // fix lỗi không render được operation - date: 02/07/2025
       }],
     };
   }
diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts
index de24d5b64a..c89f73b314 100644
--- a/client/packages/lowcoder/src/i18n/locales/en.ts
+++ b/client/packages/lowcoder/src/i18n/locales/en.ts
@@ -1938,6 +1938,7 @@ export const en = {
     "prefixIcon": "Prefix Icon",
     "prefixText": "Prefix Text",
     "suffixIcon": "Suffix Icon",
+    "textColor": "Text Color",
     "icon": "Icon",
     "iconSize": "Icon Size",
     "button": "Form Button",

From 05a70f347bbe18c4480da64d3b488644c04a8b91 Mon Sep 17 00:00:00 2001
From: triminhvnt <triminhvnt@gmail.com>
Date: Wed, 2 Jul 2025 15:44:46 +0700
Subject: [PATCH 5/7] =?UTF-8?q?b=E1=BA=ADt=20l=E1=BA=A1i=20loginemail?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../lowcoder/src/pages/userAuth/formLoginSteps.tsx        | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
index 67236d6774..f24ce69ec9 100644
--- a/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
+++ b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx
@@ -125,11 +125,11 @@ export default function FormLoginSteps(props: FormLoginProps) {
   const [pageSize, setPageSize] = useState(10);
 
   // Tạm che để cho phép đăng nhập với email
-  // const isEmailLoginEnabled = useMemo(() => {
-  //   return isFormLoginEnabled && signinEnabled;
-  // }, [isFormLoginEnabled, signinEnabled]);
+  const isEmailLoginEnabled = useMemo(() => {
+    return isFormLoginEnabled && signinEnabled;
+  }, [isFormLoginEnabled, signinEnabled]);
   
-  const isEmailLoginEnabled = true;
+  // const isEmailLoginEnabled = true;
 
   const isEnterpriseMode = useMemo(() => {
     return serverSettings?.LOWCODER_WORKSPACE_MODE === "ENTERPRISE" || serverSettings?.LOWCODER_WORKSPACE_MODE === "SINGLEWORKSPACE";

From f327e188ccbb1505c12894f933776ac2b5f18c4e Mon Sep 17 00:00:00 2001
From: minhvnt <triminhvnt@gmail.com>
Date: Sat, 5 Jul 2025 00:15:17 +0700
Subject: [PATCH 6/7] =?UTF-8?q?fix=20l=E1=BB=97i=20ATTT=20li=C3=AAn=20quan?=
 =?UTF-8?q?=20docker=20compose?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 server/api-service/docker-compose-multi.yaml | 153 -------------------
 server/api-service/docker-compose.yml        |  46 ------
 2 files changed, 199 deletions(-)
 delete mode 100644 server/api-service/docker-compose-multi.yaml
 delete mode 100644 server/api-service/docker-compose.yml

diff --git a/server/api-service/docker-compose-multi.yaml b/server/api-service/docker-compose-multi.yaml
deleted file mode 100644
index 70833c33b2..0000000000
--- a/server/api-service/docker-compose-multi.yaml
+++ /dev/null
@@ -1,153 +0,0 @@
-services:
-  mongodb:
-    image: "mongo:7.0"
-    container_name: mongodb
-    ports:
-      - "27017:27017"
-    environment:
-      MONGO_INITDB_DATABASE: lowcoder
-      MONGO_INITDB_ROOT_USERNAME: lowcoder
-      MONGO_INITDB_ROOT_PASSWORD: secret123
-    volumes:
-      - ./lowcoder-stacks/data/mongodb:/data/db
-    restart: unless-stopped
-    networks:
-      - lowcoder-network
-    healthcheck: # https://github.com/rodrigobdz/docker-compose-healthchecks?tab=readme-ov-file#mongo
-      test:
-        [
-          "CMD",
-          "mongosh",
-          "--quiet",
-          "127.0.0.1/test",
-          "--eval",
-          "'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)'",
-        ]
-      interval: 5s
-      timeout: 10s
-      retries: 10
-      start_period: 40s
-  
-  postgres:
-    image: postgres:16
-    container_name: postgres
-    environment:
-      - POSTGRES_DB=picodb
-      - POSTGRES_USER=picowood
-      - POSTGRES_PASSWORD=picowood123
-    ports:
-      - "5432:5432"
-    volumes:
-      - postgres-data:/var/lib/postgresql/data
-    restart: unless-stopped
-    networks:
-      - lowcoder-network
-    
-  redis:
-    image: redis:7-alpine
-    container_name: redis
-    ports:
-      - "6379:6379"
-    restart: unless-stopped
-    networks:
-      - lowcoder-network
-    healthcheck: # https://stackoverflow.com/a/71504657
-      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
-      interval: 1s
-      timeout: 3s
-      retries: 10
-
-  lowcoder-api-service:
-    image: lowcoderorg/lowcoder-ce-api-service:latest
-    container_name: lowcoder-api-service
-    ports:
-      - "8080:8080"
-    env_file:
-      - path: ./default.env
-        required: true
-      - path: ./default-multi.env
-        required: true
-      - path: ./override.env
-        required: false
-    restart: unless-stopped
-    networks:
-      - lowcoder-network
-    depends_on:
-      mongodb:
-        condition: service_healthy
-        restart: true
-      redis:
-        condition: service_healthy
-        restart: true
-    volumes:
-      - ./lowcoder-stacks:/lowcoder-stacks
-      - ./lowcoder-stacks/assets:/lowcoder/assets
-    healthcheck: #https://stackoverflow.com/questions/71101967/how-should-i-use-grep-in-docker-compose-healthcheck
-      test: curl -sS http://lowcoder-api-service:8080  | grep -c "Lowcoder API is up and runnig" > /dev/null
-      interval: 3s
-      timeout: 5s
-      retries: 10
-
-  lowcoder-node-service:
-    image: lowcoderorg/lowcoder-ce-node-service:latest
-    container_name: lowcoder-node-service
-    ports:
-      - "6060:6060"
-    env_file:
-      - path: ./default.env
-        required: true
-      - path: ./default-multi.env
-        required: true
-      - path: ./override.env
-        required: false
-    restart: unless-stopped
-    networks:
-      - lowcoder-network
-    depends_on:
-      lowcoder-api-service:
-        condition: service_healthy
-        restart: true
-    healthcheck: #https://stackoverflow.com/questions/71101967/how-should-i-use-grep-in-docker-compose-healthcheck
-      test: curl -sS http://lowcoder-node-service:6060  | grep -c "Lowcoder Node Service is up and running" > /dev/null
-      interval: 3s
-      timeout: 5s
-      retries: 10
-
-  lowcoder-frontend:
-    image: lowcoderorg/lowcoder-ce-frontend:latest
-    container_name: lowcoder-frontend
-    ports:
-      - "3000:3000"
-    env_file:
-      - path: ./default.env
-        required: true
-      - path: ./default-multi.env
-        required: true
-      - path: ./override.env
-        required: false
-    restart: unless-stopped
-    networks:
-      - lowcoder-network
-    depends_on:
-      lowcoder-node-service:
-        condition: service_healthy
-        restart: true
-      lowcoder-api-service:
-        condition: service_healthy
-        restart: true
-    volumes:
-      - ./lowcoder-stacks/assets:/lowcoder/assets
-      - ./lowcoder-stacks/ssl:/lowcoder-stacks/ssl
-    healthcheck:
-      test: curl --fail http://lowcoder-frontend:3000 || exit 1
-      interval: 5s
-      retries: 10
-      start_period: 10s
-      timeout: 10s
-      
-volumes:
-  postgres-data:
-  
-networks:
-  lowcoder-network:
-    driver: bridge
\ No newline at end of file
diff --git a/server/api-service/docker-compose.yml b/server/api-service/docker-compose.yml
deleted file mode 100644
index d0191aee04..0000000000
--- a/server/api-service/docker-compose.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-version: '3.8'
-
-services:
-  lowcoder:
-    image: lowcoderorg/lowcoder-ce:latest
-    container_name: lowcoder
-    env_file:
-      - path: ./default.env
-        required: true
-      - path: ./override.env
-        required: false
-    ports:
-      - "3000:3000"
-      - "3443:3443"
-      - "27017:27017"
-    volumes:
-      - ./lowcoder-stacks:/lowcoder-stacks
-      - ./lowcoder-stacks/assets:/lowcoder/assets
-    depends_on:
-      - postgres
-    networks:
-      - lowcoder-network
-    restart: always
-    
-  postgres:
-    image: postgres:16
-    container_name: postgres
-    environment:
-      - POSTGRES_DB=lowcoder
-      - POSTGRES_USER=lowcoder_user
-      - POSTGRES_PASSWORD=580784d01662f1f7e84d8c0208a5cf29996ef1ab667c775d6c7877657bb5f28f
-    ports:
-      - "5432:5432"
-    volumes:
-      - postgres-data:/var/lib/postgresql/data
-    networks:
-      - lowcoder-network
-    restart: always
-    
-volumes:
-  lowcoder-stacks:
-  postgres-data:
-
-networks:
-  lowcoder-network:
-    driver: bridge
\ No newline at end of file

From 2e28652b928cd313f7122c96a8a1c002824107d0 Mon Sep 17 00:00:00 2001
From: minhvnt <triminhvnt@gmail.com>
Date: Wed, 16 Jul 2025 23:50:51 +0700
Subject: [PATCH 7/7] =?UTF-8?q?fix=20l=E1=BB=97i=20kh=C3=B4ng=20t=E1=BA=A1?=
 =?UTF-8?q?o=20=C4=91=C6=B0=E1=BB=A3c=20nhi=E1=BB=81u=20event=20handler=20?=
 =?UTF-8?q?trong=20operation,=20links=20column=20type?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../tableComp/column/simpleColumnTypeComps.tsx  | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx
index 2e39f9e00b..39c15bdd91 100644
--- a/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx
+++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx
@@ -23,14 +23,27 @@ export const fixOldActionData = (oldData: any) => {
     return oldData; 
   }
   if (Boolean(oldData.onClick)) {
+    // Nếu là mảng, giữ nguyên
+    if (Array.isArray(oldData.onClick)) {
+      return oldData;
+    }
+    // Nếu là object, chuyển thành mảng 1 phần tử
     return {
       ...oldData,
       onClick: [{
         name: "click",
-        // handler: oldData.onClick,
-        handler: oldData.onClick[0].handler, // fix lỗi không render được operation - date: 02/07/2025
+        handler: oldData.onClick.handler || oldData.onClick[0]?.handler,
       }],
     };
+    // tạm che để fix lỗi hiển thị 1 event cho operation
+    // return {
+    //   ...oldData,
+    //   onClick: [{
+    //     name: "click",
+    //     // handler: oldData.onClick,
+    //     handler: oldData.onClick[0].handler, // fix lỗi không render được operation - date: 02/07/2025
+    //   }],
+    // };
   }
   return oldData;
 }
