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 ? ( + webcam + ) : ( + }> + + + )} + {imgSrc ? ( + + + + + ) : ( + + + setDropdownShow(value)} + popupRender={() => ( + + setVideoConstraints({ ...videoConstraints, deviceId: value.key }) + } + /> + )} + > + + + + )} + + ) + )} + + ); +}; + +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 - ? webcam - : ( - }> - - - ) - } - {imgSrc - ? ( - - - - - ) - : ( - - - setDropdownShow(value)} - popupRender={() => ( - - setVideoConstraints({ ...videoConstraints, deviceId: value.key }) - } - /> - )} - > - - - - ) - } - - ) - )} - - ) -} +// 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={() => ( - setVideoConstraints({ ...videoConstraints, deviceId: value.key }) - } + onClick={(value) => { + setVideoConstraints({ deviceId: { exact: value.key } }); + setDropdownShow(false); + }} /> )} > From af83e435bdaccb81aaf282f83f25cc6bb26a24d8 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 15 Sep 2025 16:00:03 +0500 Subject: [PATCH 9/9] #1720 fix modes height --- .../lowcoder/src/comps/comps/fileComp/fileComp.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx index 16a3b0fcb..360a81556 100644 --- a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx @@ -472,7 +472,7 @@ let FileTmpComp = new UICompBuilder(childrenMap, (props, dispatch) => { label: trans("file.dragHintText"), })} {children.uploadType.propertyView({ label: trans("file.uploadType") })} - {children.uploadMode.getView() === "dragArea" && children.autoHeight.getPropertyView()} + {children.autoHeight.getPropertyView()} @@ -530,11 +530,7 @@ let FileTmpComp = new UICompBuilder(childrenMap, (props, dispatch) => { 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 + // Both button and dragArea modes should respect the autoHeight setting const h = this.children.autoHeight.getView(); return h; }