From 2c05e4d8197c0c8285a0467e63d831f798a412f3 Mon Sep 17 00:00:00 2001
From: Faran Javed
Date: Thu, 11 Sep 2025 23:23:07 +0500
Subject: [PATCH 1/9] [Feat]: #1720 add upload dragger
---
.../comps/comps/fileComp/draggerUpload.tsx | 213 ++++++++++++++++++
.../src/comps/comps/fileComp/fileComp.tsx | 28 ++-
.../packages/lowcoder/src/i18n/locales/en.ts | 5 +
3 files changed, 242 insertions(+), 4 deletions(-)
create mode 100644 client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
new file mode 100644
index 000000000..db986bbf4
--- /dev/null
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
@@ -0,0 +1,213 @@
+import { default as AntdUpload } from "antd/es/upload";
+import { default as Button } from "antd/es/button";
+import { UploadFile, UploadChangeParam } from "antd/es/upload/interface";
+import { useState, useEffect } from "react";
+import styled from "styled-components";
+import { trans } from "i18n";
+import _ from "lodash";
+import {
+ changeValueAction,
+ CompAction,
+ multiChangeAction,
+ RecordConstructorToView,
+} from "lowcoder-core";
+import { hasIcon } from "comps/utils";
+import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
+import { resolveValue, resolveParsedValue, commonProps } from "./fileComp";
+import { FileStyleType } from "comps/controls/styleControlConstants";
+
+const IconWrapper = styled.span`
+ display: flex;
+`;
+
+const StyledDragger = styled(AntdUpload.Dragger)<{
+ $style: FileStyleType;
+ $autoHeight: boolean;
+}>`
+ &.ant-upload-drag {
+ border-color: ${(props) => props.$style.border};
+ border-width: ${(props) => props.$style.borderWidth};
+ border-style: ${(props) => props.$style.borderStyle};
+ border-radius: ${(props) => props.$style.radius};
+ background: ${(props) => props.$style.background};
+ ${(props) => !props.$autoHeight && `height: 200px; display: flex; align-items: center;`}
+
+ .ant-upload-drag-container {
+ ${(props) => !props.$autoHeight && `display: flex; flex-direction: column; justify-content: center; height: 100%;`}
+ }
+
+ &:hover {
+ border-color: ${(props) => props.$style.accent};
+ background: ${(props) => props.$style.background};
+ }
+
+ .ant-upload-text {
+ color: ${(props) => props.$style.text};
+ font-size: ${(props) => props.$style.textSize};
+ font-weight: ${(props) => props.$style.textWeight};
+ font-family: ${(props) => props.$style.fontFamily};
+ font-style: ${(props) => props.$style.fontStyle};
+ }
+
+ .ant-upload-hint {
+ color: ${(props) => props.$style.text};
+ opacity: 0.7;
+ }
+
+ .ant-upload-drag-icon {
+ margin-bottom: 16px;
+
+ .anticon {
+ color: ${(props) => props.$style.accent};
+ font-size: 48px;
+ }
+ }
+ }
+`;
+
+interface DraggerUploadProps {
+ value: Array;
+ files: any[];
+ fileType: string[];
+ showUploadList: boolean;
+ disabled: boolean;
+ onEvent: (eventName: string) => void;
+ style: FileStyleType;
+ parseFiles: boolean;
+ parsedValue: Array;
+ prefixIcon: any;
+ suffixIcon: any;
+ forceCapture: boolean;
+ minSize: number;
+ maxSize: number;
+ maxFiles: number;
+ uploadType: "single" | "multiple" | "directory";
+ text: string;
+ dispatch: (action: CompAction) => void;
+ autoHeight: boolean;
+ tabIndex?: number;
+}
+
+export const DraggerUpload = (props: DraggerUploadProps) => {
+ const { dispatch, files, style, autoHeight } = props;
+ const [fileList, setFileList] = useState(
+ files.map((f) => ({ ...f, status: "done" })) as UploadFile[]
+ );
+
+ useEffect(() => {
+ if (files.length === 0 && fileList.length !== 0) {
+ setFileList([]);
+ }
+ }, [files]);
+
+ const handleOnChange = (param: UploadChangeParam) => {
+ const uploadingFiles = param.fileList.filter((f) => f.status === "uploading");
+ 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;
+ }
+
+ 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);
+
+ 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={handleOnChange}
+ >
+
+ {hasIcon(props.prefixIcon) ? (
+ {props.prefixIcon}
+ ) : (
+
+ )}
+
+
+ {props.text || trans("file.dragAreaText")}
+
+
+ {trans("file.dragAreaHint")}
+
+
+ );
+};
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
index 8580e2d5e..bc7f30da6 100644
--- a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
@@ -42,6 +42,7 @@ import { CommonNameConfig, NameConfig, withExposingConfigs } from "../../generat
import { formDataChildren, FormDataPropertyView } from "../formComp/formDataConstants";
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
import { CustomModal } from "lowcoder-design";
+import { DraggerUpload } from "./draggerUpload";
import React, { useContext } from "react";
import { EditorContext } from "comps/editorState";
@@ -50,6 +51,7 @@ import Skeleton from "antd/es/skeleton";
import Menu from "antd/es/menu";
import Flex from "antd/es/flex";
import { checkIsMobile } from "@lowcoder-ee/util/commonUtils";
+import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl";
const FileSizeControl = codeControl((value) => {
if (typeof value === "number") {
@@ -131,7 +133,7 @@ const commonValidationFields = (children: RecordConstructorToComp & {
uploadType: "single" | "multiple" | "directory";
}
@@ -619,25 +621,43 @@ const UploadTypeOptions = [
{ label: trans("file.directory"), value: "directory" },
] as const;
+const UploadModeOptions = [
+ { label: trans("file.button"), value: "button" },
+ { label: trans("file.dragArea"), value: "dragArea" },
+] as const;
+
const childrenMap = {
text: withDefault(StringControl, trans("file.upload")),
uploadType: dropdownControl(UploadTypeOptions, "single"),
+ uploadMode: dropdownControl(UploadModeOptions, "button"),
+ autoHeight: withDefault(AutoHeightControl, "fixed"),
tabIndex: NumberControl,
...commonChildren,
...formDataChildren,
};
let FileTmpComp = new UICompBuilder(childrenMap, (props, dispatch) => {
- return(
-
- )})
+ const uploadMode = props.uploadMode;
+ const autoHeight = props.autoHeight;
+
+ if (uploadMode === "dragArea") {
+ return ;
+ }
+
+ return ;
+})
.setPropertyViewFn((children) => (
<>
{children.text.propertyView({
label: trans("text"),
})}
+ {children.uploadMode.propertyView({
+ label: trans("file.uploadMode"),
+ radioButton: true,
+ })}
{children.uploadType.propertyView({ label: trans("file.uploadType") })}
+ {children.uploadMode.getView() === "dragArea" && children.autoHeight.getPropertyView()}
diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts
index 80f667288..3c0b935f7 100644
--- a/client/packages/lowcoder/src/i18n/locales/en.ts
+++ b/client/packages/lowcoder/src/i18n/locales/en.ts
@@ -1925,6 +1925,11 @@ export const en = {
"usePhoto": "Use Photo",
"retakePhoto": "Retake Photo",
"capture": "Capture",
+ "button": "Button",
+ "dragArea": "Drag Area",
+ "uploadMode": "Upload Mode",
+ "dragAreaText": "Click or drag file to this area to upload",
+ "dragAreaHint": "Support for a single or bulk upload. Strictly prohibited from uploading company data or other band files.",
},
"date": {
"format": "Format",
From 5407edc23e2910e10c324eda951a3ebef3419136 Mon Sep 17 00:00:00 2001
From: Faran Javed
Date: Fri, 12 Sep 2025 13:11:04 +0500
Subject: [PATCH 2/9] #1720 fix type error
---
.../lowcoder/src/comps/comps/fileComp/draggerUpload.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
index db986bbf4..9ced1e035 100644
--- a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
@@ -14,7 +14,7 @@ import {
import { hasIcon } from "comps/utils";
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
import { resolveValue, resolveParsedValue, commonProps } from "./fileComp";
-import { FileStyleType } from "comps/controls/styleControlConstants";
+import { FileStyleType, AnimationStyleType } from "comps/controls/styleControlConstants";
const IconWrapper = styled.span`
display: flex;
@@ -71,8 +71,9 @@ interface DraggerUploadProps {
fileType: string[];
showUploadList: boolean;
disabled: boolean;
- onEvent: (eventName: string) => void;
+ onEvent: (eventName: string) => Promise;
style: FileStyleType;
+ animationStyle: AnimationStyleType;
parseFiles: boolean;
parsedValue: Array;
prefixIcon: any;
From 5464c858d40bd8f847e951877fa2c40461c1f7b7 Mon Sep 17 00:00:00 2001
From: Faran Javed
Date: Fri, 12 Sep 2025 14:41:36 +0500
Subject: [PATCH 3/9] #1720 add adjustable height in fixed mode
---
.../src/comps/comps/fileComp/fileComp.tsx | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
index bc7f30da6..6cb8fb4ad 100644
--- a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
@@ -713,7 +713,19 @@ let FileTmpComp = new UICompBuilder(childrenMap, (props, dispatch) => {
))
.build();
-FileTmpComp = withMethodExposing(FileTmpComp, [
+ class FileImplComp extends FileTmpComp {
+ override autoHeight(): boolean {
+ // Button mode is always fixed (grid should show resize handles)
+ const mode = this.children.uploadMode.getView(); // "button" | "dragArea"
+ if (mode !== "dragArea") return false;
+
+ // "auto" | "fixed" -> boolean
+ const h = this.children.autoHeight.getView();
+ return h;
+ }
+ }
+
+const FileWithMethods = withMethodExposing(FileImplComp, [
{
method: {
name: "clearValue",
@@ -731,7 +743,7 @@ FileTmpComp = withMethodExposing(FileTmpComp, [
},
]);
-export const FileComp = withExposingConfigs(FileTmpComp, [
+export const FileComp = withExposingConfigs(FileWithMethods, [
new NameConfig("value", trans("file.filesValueDesc")),
new NameConfig(
"files",
From 85e9cf5e24a5bb58859332a75b11224b6e9d13de Mon Sep 17 00:00:00 2001
From: Faran Javed
Date: Fri, 12 Sep 2025 16:17:42 +0500
Subject: [PATCH 4/9] #1720 fix UI
---
.../comps/comps/fileComp/draggerUpload.tsx | 177 ++++++++++--------
1 file changed, 104 insertions(+), 73 deletions(-)
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
index 9ced1e035..ed3b6e286 100644
--- a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
@@ -9,7 +9,6 @@ import {
changeValueAction,
CompAction,
multiChangeAction,
- RecordConstructorToView,
} from "lowcoder-core";
import { hasIcon } from "comps/utils";
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
@@ -20,51 +19,81 @@ const IconWrapper = styled.span`
display: flex;
`;
+const DraggerShell = styled.div<{ $auto: boolean }>`
+ height: ${(p) => (p.$auto ? "auto" : "100%")};
+
+ /* AntD wraps dragger + list in this */
+ .ant-upload-wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: auto; /* allows list to be visible if it grows */
+ }
+
+ /* The drag area itself */
+ .ant-upload-drag {
+ ${(p) =>
+ !p.$auto &&
+ `
+ flex: 1 1 auto;
+ min-height: 120px;
+ min-width: 0;
+ `}
+ }
+
+ /* The list sits below the dragger */
+ .ant-upload-list {
+ ${(p) =>
+ !p.$auto &&
+ `
+ flex: 0 0 auto;
+ `}
+ }
+`;
+
+
const StyledDragger = styled(AntdUpload.Dragger)<{
$style: FileStyleType;
- $autoHeight: boolean;
+ $auto: boolean;
}>`
&.ant-upload-drag {
- border-color: ${(props) => props.$style.border};
- border-width: ${(props) => props.$style.borderWidth};
- border-style: ${(props) => props.$style.borderStyle};
- border-radius: ${(props) => props.$style.radius};
- background: ${(props) => props.$style.background};
- ${(props) => !props.$autoHeight && `height: 200px; display: flex; align-items: center;`}
-
+ border-color: ${(p) => p.$style.border};
+ border-width: ${(p) => p.$style.borderWidth};
+ border-style: ${(p) => p.$style.borderStyle};
+ border-radius: ${(p) => p.$style.radius};
+ background: ${(p) => p.$style.background};
+
+ ${(p) =>
+ !p.$auto &&
+ `
+ display: flex;
+ align-items: center;
+ `}
+
+ ${(p) =>
+ p.$auto &&
+ `
+ min-height: 200px;
+ `}
+
.ant-upload-drag-container {
- ${(props) => !props.$autoHeight && `display: flex; flex-direction: column; justify-content: center; height: 100%;`}
+ ${(p) =>
+ !p.$auto &&
+ `
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ height: 100%;
+ `}
}
-
+
&:hover {
- border-color: ${(props) => props.$style.accent};
- background: ${(props) => props.$style.background};
- }
-
- .ant-upload-text {
- color: ${(props) => props.$style.text};
- font-size: ${(props) => props.$style.textSize};
- font-weight: ${(props) => props.$style.textWeight};
- font-family: ${(props) => props.$style.fontFamily};
- font-style: ${(props) => props.$style.fontStyle};
- }
-
- .ant-upload-hint {
- color: ${(props) => props.$style.text};
- opacity: 0.7;
- }
-
- .ant-upload-drag-icon {
- margin-bottom: 16px;
-
- .anticon {
- color: ${(props) => props.$style.accent};
- font-size: 48px;
- }
+ border-color: ${(p) => p.$style.accent};
}
}
`;
+
interface DraggerUploadProps {
value: Array;
files: any[];
@@ -172,43 +201,45 @@ export const DraggerUpload = (props: DraggerUploadProps) => {
};
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={handleOnChange}
- >
-
- {hasIcon(props.prefixIcon) ? (
- {props.prefixIcon}
- ) : (
-
- )}
-
-
- {props.text || trans("file.dragAreaText")}
-
-
- {trans("file.dragAreaHint")}
-
-
+
+ {
+ 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={handleOnChange}
+ >
+
+ {hasIcon(props.prefixIcon) ? (
+ {props.prefixIcon}
+ ) : (
+
+ )}
+
+
+ {props.text || trans("file.dragAreaText")}
+
+
+ {trans("file.dragAreaHint")}
+
+
+
);
};
From edbd09d1193242ec9f3ca1a17625605f410bac15 Mon Sep 17 00:00:00 2001
From: Faran Javed
Date: Fri, 12 Sep 2025 16:53:09 +0500
Subject: [PATCH 5/9] #1720 add drag area hint
---
.../lowcoder/src/comps/comps/fileComp/draggerUpload.tsx | 3 ++-
.../packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx | 6 ++++++
client/packages/lowcoder/src/i18n/locales/en.ts | 1 +
3 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
index ed3b6e286..9d22926df 100644
--- a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
@@ -113,6 +113,7 @@ interface DraggerUploadProps {
maxFiles: number;
uploadType: "single" | "multiple" | "directory";
text: string;
+ dragHintText?: string;
dispatch: (action: CompAction) => void;
autoHeight: boolean;
tabIndex?: number;
@@ -237,7 +238,7 @@ export const DraggerUpload = (props: DraggerUploadProps) => {
{props.text || trans("file.dragAreaText")}
- {trans("file.dragAreaHint")}
+ {props.dragHintText}
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
index 6cb8fb4ad..36e1691a8 100644
--- a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
@@ -449,6 +449,7 @@ const Upload = (
props: RecordConstructorToView & {
uploadType: "single" | "multiple" | "directory";
text: string;
+ dragHintText?: string;
dispatch: (action: CompAction) => void;
forceCapture: boolean;
tabIndex?: number;
@@ -628,6 +629,7 @@ const UploadModeOptions = [
const childrenMap = {
text: withDefault(StringControl, trans("file.upload")),
+ dragHintText: withDefault(StringControl, trans("file.dragAreaHint")),
uploadType: dropdownControl(UploadTypeOptions, "single"),
uploadMode: dropdownControl(UploadModeOptions, "button"),
autoHeight: withDefault(AutoHeightControl, "fixed"),
@@ -656,6 +658,10 @@ let FileTmpComp = new UICompBuilder(childrenMap, (props, dispatch) => {
label: trans("file.uploadMode"),
radioButton: true,
})}
+ {children.uploadMode.getView() === "dragArea" &&
+ children.dragHintText.propertyView({
+ label: trans("file.dragHintText"),
+ })}
{children.uploadType.propertyView({ label: trans("file.uploadType") })}
{children.uploadMode.getView() === "dragArea" && children.autoHeight.getPropertyView()}
diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts
index 3c0b935f7..43311e49d 100644
--- a/client/packages/lowcoder/src/i18n/locales/en.ts
+++ b/client/packages/lowcoder/src/i18n/locales/en.ts
@@ -1930,6 +1930,7 @@ export const en = {
"uploadMode": "Upload Mode",
"dragAreaText": "Click or drag file to this area to upload",
"dragAreaHint": "Support for a single or bulk upload. Strictly prohibited from uploading company data or other band files.",
+ "dragHintText": "Hint Text",
},
"date": {
"format": "Format",
From 5db3f5ae359552c484aa93b0782b37c95e22a5cf Mon Sep 17 00:00:00 2001
From: Faran Javed
Date: Fri, 12 Sep 2025 17:31:08 +0500
Subject: [PATCH 6/9] #1720 remove unnecessary styled wrapper
---
.../comps/comps/fileComp/draggerUpload.tsx | 63 ++++---------------
1 file changed, 13 insertions(+), 50 deletions(-)
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
index 9d22926df..1e775d895 100644
--- a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
@@ -19,11 +19,13 @@ const IconWrapper = styled.span`
display: flex;
`;
-const DraggerShell = styled.div<{ $auto: boolean }>`
+const DraggerShell = styled(AntdUpload.Dragger)<{ $auto: boolean
+ $style: FileStyleType
+}>`
height: ${(p) => (p.$auto ? "auto" : "100%")};
/* AntD wraps dragger + list in this */
- .ant-upload-wrapper {
+ &.ant-upload-wrapper {
display: flex;
flex-direction: column;
height: 100%;
@@ -39,6 +41,12 @@ const DraggerShell = styled.div<{ $auto: boolean }>`
min-height: 120px;
min-width: 0;
`}
+ .ant-upload-drag-container {
+ .ant-upload-drag-icon {
+ display: flex;
+ justify-content: center;
+ }
+ }
}
/* The list sits below the dragger */
@@ -50,50 +58,6 @@ const DraggerShell = styled.div<{ $auto: boolean }>`
`}
}
`;
-
-
-const StyledDragger = styled(AntdUpload.Dragger)<{
- $style: FileStyleType;
- $auto: boolean;
-}>`
- &.ant-upload-drag {
- border-color: ${(p) => p.$style.border};
- border-width: ${(p) => p.$style.borderWidth};
- border-style: ${(p) => p.$style.borderStyle};
- border-radius: ${(p) => p.$style.radius};
- background: ${(p) => p.$style.background};
-
- ${(p) =>
- !p.$auto &&
- `
- display: flex;
- align-items: center;
- `}
-
- ${(p) =>
- p.$auto &&
- `
- min-height: 200px;
- `}
-
- .ant-upload-drag-container {
- ${(p) =>
- !p.$auto &&
- `
- display: flex;
- flex-direction: column;
- justify-content: center;
- height: 100%;
- `}
- }
-
- &:hover {
- border-color: ${(p) => p.$style.accent};
- }
- }
-`;
-
-
interface DraggerUploadProps {
value: Array;
files: any[];
@@ -202,8 +166,7 @@ export const DraggerUpload = (props: DraggerUploadProps) => {
};
return (
-
- {
{props.dragHintText}
-
-
+
+
);
};
From 43c7fa3cfa5604dac7df02f80b9d687245e9afa3 Mon Sep 17 00:00:00 2001
From: Faran Javed
Date: Fri, 12 Sep 2025 21:46:38 +0500
Subject: [PATCH 7/9] #1720 add dragger image capture modal + custom dragger
styling
---
.../comps/fileComp/ImageCaptureModal.tsx | 197 +++++++++++++++++
.../comps/comps/fileComp/draggerUpload.tsx | 163 ++++++++++++--
.../src/comps/comps/fileComp/fileComp.tsx | 205 +-----------------
3 files changed, 344 insertions(+), 221 deletions(-)
create mode 100644 client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx
new file mode 100644
index 000000000..0caba93eb
--- /dev/null
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx
@@ -0,0 +1,197 @@
+import React, { Suspense, useCallback, useEffect, useRef, useState } from "react";
+import { default as Button } from "antd/es/button";
+import Dropdown from "antd/es/dropdown";
+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 styled from "styled-components";
+import { trans } from "i18n";
+import { CustomModal } from "lowcoder-design";
+
+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;
+ }
+ }
+`;
+
+const ReactWebcam = React.lazy(() => import("react-webcam"));
+
+export 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 ? (
+
+ ) : (
+ }>
+
+
+ )}
+ {imgSrc ? (
+
+
+
+
+ ) : (
+
+
+ setDropdownShow(value)}
+ popupRender={() => (
+
+
+ )}
+
+ )
+ )}
+
+ );
+};
+
+export default ImageCaptureModal;
+
+
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
index 1e775d895..2f230ad38 100644
--- a/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/draggerUpload.tsx
@@ -1,8 +1,8 @@
import { default as AntdUpload } from "antd/es/upload";
import { default as Button } from "antd/es/button";
-import { UploadFile, UploadChangeParam } from "antd/es/upload/interface";
+import { UploadFile, UploadChangeParam, UploadFileStatus, RcFile } from "antd/es/upload/interface";
import { useState, useEffect } from "react";
-import styled from "styled-components";
+import styled, { css } from "styled-components";
import { trans } from "i18n";
import _ from "lodash";
import {
@@ -13,16 +13,85 @@ import {
import { hasIcon } from "comps/utils";
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
import { resolveValue, resolveParsedValue, commonProps } from "./fileComp";
-import { FileStyleType, AnimationStyleType } from "comps/controls/styleControlConstants";
+import { FileStyleType, AnimationStyleType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants";
+import { ImageCaptureModal } from "./ImageCaptureModal";
+import { v4 as uuidv4 } from "uuid";
+import { checkIsMobile } from "@lowcoder-ee/util/commonUtils";
+import { darkenColor } from "components/colorSelect/colorUtils";
const IconWrapper = styled.span`
display: flex;
`;
-const DraggerShell = styled(AntdUpload.Dragger)<{ $auto: boolean
- $style: FileStyleType
+const getDraggerStyle = (style: FileStyleType) => {
+ return css`
+ .ant-upload-drag {
+ border-radius: ${style.radius};
+ rotate: ${style.rotation};
+ margin: ${style.margin};
+ padding: ${style.padding};
+ width: ${widthCalculator(style.margin)};
+ height: ${heightCalculator(style.margin)};
+ border-width: ${style.borderWidth};
+ border-style: ${style.borderStyle};
+ border-color: ${style.border};
+ background: ${style.background};
+ transition: all 0.3s;
+ .ant-upload-drag-container {
+ .ant-upload-text {
+ color: ${style.text};
+ font-family: ${style.fontFamily};
+ font-size: ${style.textSize};
+ font-weight: ${style.textWeight};
+ font-style: ${style.fontStyle};
+ text-decoration: ${style.textDecoration};
+ text-transform: ${style.textTransform};
+ }
+
+ .ant-upload-hint {
+ color: ${darkenColor(style.text, 0.3)};
+ font-family: ${style.fontFamily};
+ font-size: calc(${style.textSize} * 0.9);
+ }
+
+ .ant-upload-drag-icon {
+ span {
+ color: ${style.accent};
+ }
+ }
+ }
+ }
+
+ .ant-upload-list {
+ .ant-upload-list-item {
+ border-color: ${style.border};
+
+ .ant-upload-list-item-name {
+ color: ${style.text};
+ }
+ }
+ }
+ `;
+};
+
+const DragAreaOverlay = styled.div`
+ // make it position fixed, transparent and match the parent
+ position: fixed;
+ background-color: transparent;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ top: 0;
+ left: 0;
+`;
+
+const StyledDraggerUpload = styled(AntdUpload.Dragger)<{
+ $auto: boolean;
+ $style: FileStyleType;
+ $animationStyle: AnimationStyleType;
}>`
height: ${(p) => (p.$auto ? "auto" : "100%")};
+ position: relative;
/* AntD wraps dragger + list in this */
&.ant-upload-wrapper {
@@ -41,6 +110,9 @@ const DraggerShell = styled(AntdUpload.Dragger)<{ $auto: boolean
min-height: 120px;
min-width: 0;
`}
+ position: relative;
+ ${(props) => props.$animationStyle}
+
.ant-upload-drag-container {
.ant-upload-drag-icon {
display: flex;
@@ -56,7 +128,12 @@ const DraggerShell = styled(AntdUpload.Dragger)<{ $auto: boolean
`
flex: 0 0 auto;
`}
+ position: relative;
+ z-index: 2;
}
+
+ /* Apply custom styling */
+ ${(props) => props.$style && getDraggerStyle(props.$style)}
`;
interface DraggerUploadProps {
value: Array;
@@ -84,10 +161,12 @@ interface DraggerUploadProps {
}
export const DraggerUpload = (props: DraggerUploadProps) => {
- const { dispatch, files, style, autoHeight } = props;
+ const { dispatch, files, style, autoHeight, animationStyle } = 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) {
@@ -166,11 +245,15 @@ export const DraggerUpload = (props: DraggerUploadProps) => {
};
return (
-
+ {
if (!file.size || file.size <= 0) {
messageInstance.error(`${file.name} ` + trans("file.fileEmptyErrorMsg"));
@@ -188,22 +271,56 @@ export const DraggerUpload = (props: DraggerUploadProps) => {
}}
onChange={handleOnChange}
>
-
- {hasIcon(props.prefixIcon) ? (
- {props.prefixIcon}
- ) : (
-
+
+ {hasIcon(props.prefixIcon) ? (
+ {props.prefixIcon}
+ ) : (
+
+ )}
+
+
+ {props.text || trans("file.dragAreaText")}
+
+
+ {props.dragHintText}
+
+ {/* we need a custom overlay to add the onClick handler */}
+ {props.forceCapture && !isMobile && (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ setShowModal(true);
+ }}
+ />
)}
-
-
- {props.text || trans("file.dragAreaText")}
-
-
- {props.dragHintText}
-
-
-
+
+
+
+ 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 = uuidv4();
+ 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] });
+ }}
+ />
+ >
);
};
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
index 36e1691a8..16a3b0fcb 100644
--- a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx
@@ -43,13 +43,9 @@ import { formDataChildren, FormDataPropertyView } from "../formComp/formDataCons
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
import { CustomModal } from "lowcoder-design";
import { DraggerUpload } from "./draggerUpload";
-
-import React, { useContext } from "react";
+import { ImageCaptureModal } from "./ImageCaptureModal";
+import { 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/util/commonUtils";
import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl";
@@ -213,45 +209,9 @@ 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;
- }
- }
-`;
+const CustomModalStyled = styled(CustomModal)``;
+const Error = styled.div``;
+const Wrapper = styled.div``;
export function resolveValue(files: UploadFile[]) {
return Promise.all(
@@ -292,158 +252,7 @@ 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
- ?
- : (
- }>
-
-
- )
- }
- {imgSrc
- ? (
-
-
-
-
- )
- : (
-
-
- setDropdownShow(value)}
- popupRender={() => (
-
-
- )
- }
-
- )
- )}
-
- )
-}
+// ImageCaptureModal moved to its own file for reuse
const Upload = (
props: RecordConstructorToView & {
@@ -632,7 +441,7 @@ const childrenMap = {
dragHintText: withDefault(StringControl, trans("file.dragAreaHint")),
uploadType: dropdownControl(UploadTypeOptions, "single"),
uploadMode: dropdownControl(UploadModeOptions, "button"),
- autoHeight: withDefault(AutoHeightControl, "fixed"),
+ autoHeight: withDefault(AutoHeightControl, "auto"),
tabIndex: NumberControl,
...commonChildren,
...formDataChildren,
From 1e09cba2098b4c1640a729545964d52fff07ff0d Mon Sep 17 00:00:00 2001
From: Faran Javed
Date: Mon, 15 Sep 2025 13:20:36 +0500
Subject: [PATCH 8/9] #1976 fix switch camera functionality
---
.../src/comps/comps/fileComp/ImageCaptureModal.tsx | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx
index 0caba93eb..ea35e2c52 100644
--- a/client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx
+++ b/client/packages/lowcoder/src/comps/comps/fileComp/ImageCaptureModal.tsx
@@ -69,6 +69,8 @@ export const ImageCaptureModal = (props: {
if (props.showModal) {
setImgSrc("");
setErrMessage("");
+ setVideoConstraints({ facingMode: "environment" });
+ setDropdownShow(false);
}
}, [props.showModal]);
@@ -119,9 +121,11 @@ export const ImageCaptureModal = (props: {
) : (
}>
)}
@@ -167,9 +171,10 @@ export const ImageCaptureModal = (props: {
popupRender={() => (