From 0639f1bc80bb27edf30fc3a1da8cee571e9fb62f Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Tue, 2 Jul 2024 10:36:37 +0500 Subject: [PATCH 01/18] integrate iconscout in icon button and image comp --- .../packages/lowcoder/src/api/iconscoutApi.ts | 80 +++ .../lowcoder/src/comps/comps/imageComp.tsx | 25 +- .../comps/comps/jsonComp/jsonLottieComp.tsx | 22 +- .../comps/comps/meetingComp/controlButton.tsx | 36 +- .../src/comps/controls/iconscoutControl.tsx | 611 ++++++++++++++++++ 5 files changed, 766 insertions(+), 8 deletions(-) create mode 100644 client/packages/lowcoder/src/api/iconscoutApi.ts create mode 100644 client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx diff --git a/client/packages/lowcoder/src/api/iconscoutApi.ts b/client/packages/lowcoder/src/api/iconscoutApi.ts new file mode 100644 index 000000000..8f947d88f --- /dev/null +++ b/client/packages/lowcoder/src/api/iconscoutApi.ts @@ -0,0 +1,80 @@ +import Api from "api/api"; +import axios, { AxiosInstance, AxiosPromise, AxiosRequestConfig } from "axios"; +import { GenericApiResponse } from "./apiResponses"; + +export interface SearchParams { + query: string; + product_type: string; + asset: string; + per_page: number; + page: 1; + sort: string; + formats: string; +} + +export type ResponseType = { + response: any; +}; + +const apiUrl = "https://api.iconscout.com"; +const clientID = ""; //"91870410585071"; +const clientSecret = ""; // "GV5aCWpwdLWTxVXFBjMKSoyDPUyjzXLR"; +const currentPage = 1; +const currentQuery = ''; +const currentData = []; + +let axiosIns: AxiosInstance | null = null; + +const getAxiosInstance = (clientSecret?: string) => { + if (axiosIns && !clientSecret) { + return axiosIns; + } + + const headers: Record = { + "Content-Type": "application/json", + "Client-ID": clientID, + } + if (clientSecret) { + headers['Client-Secret'] = clientSecret; + } + const apiRequestConfig: AxiosRequestConfig = { + baseURL: `${apiUrl}`, + headers, + withCredentials: true, + }; + + axiosIns = axios.create(apiRequestConfig); + return axiosIns; +} + +class IconscoutApi extends Api { + static async search(params: SearchParams): Promise { + let response; + try { + response = await getAxiosInstance().request({ + url: '/v3/search', + method: "GET", + withCredentials: false, + params: { + ...params, + 'formats[]': params.formats, + }, + }); + } catch (error) { + console.error(error); + } + return response?.data.response.items; + } + + static async download(uuid: string, params: Record): Promise { + const response = await getAxiosInstance(clientSecret).request({ + url: `/v3/items/${uuid}/api-download`, + method: "POST", + withCredentials: false, + params, + }); + return response?.data.response.download; + } +} + +export default IconscoutApi; diff --git a/client/packages/lowcoder/src/comps/comps/imageComp.tsx b/client/packages/lowcoder/src/comps/comps/imageComp.tsx index d78a21d20..7456fc2ff 100644 --- a/client/packages/lowcoder/src/comps/comps/imageComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/imageComp.tsx @@ -12,7 +12,7 @@ import { withExposingConfigs, } from "../generators/withExposing"; import { RecordConstructorToView } from "lowcoder-core"; -import { useEffect, useRef, useState } from "react"; +import { ReactElement, useEffect, useRef, useState } from "react"; import _ from "lodash"; import ReactResizeDetector from "react-resize-detector"; import { styleControl } from "comps/controls/styleControl"; @@ -35,6 +35,8 @@ import { useContext } from "react"; import { EditorContext } from "comps/editorState"; import { StringControl } from "../controls/codeControl"; import { PositionControl } from "comps/controls/dropdownControl"; +import { dropdownControl } from "../controls/dropdownControl"; +import { IconScoutAssetType, IconscoutControl } from "../controls/iconscoutControl"; const Container = styled.div<{ $style: ImageStyleType | undefined, @@ -111,6 +113,10 @@ const getStyle = (style: ImageStyleType) => { }; const EventOptions = [clickEvent] as const; +const ModeOptions = [ + { label: "URL", value: "standard" }, + { label: "Advanced", value: "advanced" }, +] as const; const ContainerImg = (props: RecordConstructorToView) => { const imgRef = useRef(null); @@ -194,7 +200,11 @@ const ContainerImg = (props: RecordConstructorToView) => { } > ) => { }; const childrenMap = { + sourceMode: dropdownControl(ModeOptions, "standard"), src: withDefault(StringStateControl, "https://temp.im/350x400"), + srcIconScout: IconscoutControl(IconScoutAssetType.ILLUSTRATION), onEvent: eventHandlerControl(EventOptions), style: styleControl(ImageStyle , 'style'), animationStyle: styleControl(AnimationStyle , 'animationStyle'), @@ -234,7 +246,14 @@ let ImageBasicComp = new UICompBuilder(childrenMap, (props) => { return ( <>
- {children.src.propertyView({ + { children.sourceMode.propertyView({ + label: "", + radioButton: true + })} + {children.sourceMode.getView() === 'standard' && children.src.propertyView({ + label: trans("image.src"), + })} + {children.sourceMode.getView() === 'advanced' &&children.srcIconScout.propertyView({ label: trans("image.src"), })}
diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index c3f93b6e1..cfaf06bca 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx @@ -18,6 +18,7 @@ import { } from "../../generators/withExposing"; import { defaultLottie } from "./jsonConstants"; import { EditorContext } from "comps/editorState"; +import { IconScoutAssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; const Player = lazy( () => import('@lottiefiles/react-lottie-player') @@ -84,12 +85,20 @@ const speedOptions = [ }, ] as const; +const ModeOptions = [ + { label: "Data", value: "standard" }, + { label: "Advanced", value: "advanced" }, +] as const; + let JsonLottieTmpComp = (function () { const childrenMap = { + sourceMode: dropdownControl(ModeOptions, "standard"), value: withDefault( ArrayOrJSONObjectControl, JSON.stringify(defaultLottie, null, 2) ), + srcIconScout: IconscoutControl(IconScoutAssetType.LOTTIE), + valueIconScout: ArrayOrJSONObjectControl, speed: dropdownControl(speedOptions, "1"), width: withDefault(NumberControl, 100), height: withDefault(NumberControl, 100), @@ -100,6 +109,7 @@ let JsonLottieTmpComp = (function () { keepLastFrame: BoolControl.DEFAULT_TRUE, }; return new UICompBuilder(childrenMap, (props) => { + console.log(props.srcIconScout); return (
- {children.value.propertyView({ + { children.sourceMode.propertyView({ + label: "", + radioButton: true + })} + {children.sourceMode.getView() === 'standard' && children.value.propertyView({ + label: trans("jsonLottie.lottieJson"), + })} + {children.sourceMode.getView() === 'advanced' && children.srcIconScout.propertyView({ + label: "Lottie Source", + })} + {children.sourceMode.getView() === 'advanced' && children.valueIconScout.propertyView({ label: trans("jsonLottie.lottieJson"), })}
diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx index 28e318618..f8edb43d6 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx @@ -39,6 +39,7 @@ import { useEffect, useRef, useState } from "react"; import ReactResizeDetector from "react-resize-detector"; import { useContext } from "react"; +import { IconScoutAssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; const Container = styled.div<{ $style: any }>` height: 100%; @@ -74,6 +75,13 @@ const IconWrapper = styled.div<{ $style: any }>` ${(props) => props.$style && getStyleIcon(props.$style)} `; +const IconScoutWrapper = styled.div<{ $style: any }>` + display: flex; + height: 100%; + + ${(props) => props.$style && getStyleIcon(props.$style)} +`; + function getStyleIcon(style: any) { return css` svg { @@ -163,6 +171,11 @@ const typeOptions = [ }, ] as const; +const ModeOptions = [ + { label: "Standard", value: "standard" }, + { label: "Advanced", value: "advanced" }, +] as const; + function isDefault(type?: string) { return !type; } @@ -183,7 +196,9 @@ const childrenMap = { disabled: BoolCodeControl, loading: BoolCodeControl, form: SelectFormControl, + sourceMode: dropdownControl(ModeOptions, "standard"), prefixIcon: IconControl, + prefixIconScout: IconscoutControl(IconScoutAssetType.ICON), style: ButtonStyleControl, viewRef: RefControl, restrictPaddingOnRotation:withDefault(StringControl, 'controlButton') @@ -226,7 +241,7 @@ let ButtonTmpComp = (function () { setStyle(container?.clientHeight + "px", container?.clientWidth + "px"); }; - + console.log(props.prefixIconScout); return ( {(editorState) => ( @@ -270,14 +285,20 @@ let ButtonTmpComp = (function () { : submitForm(editorState, props.form) } > - {props.prefixIcon && ( + {props.sourceMode === 'standard' && props.prefixIcon && ( {props.prefixIcon} )} - + {props.sourceMode === 'advanced' && props.prefixIconScout && ( + + {props.prefixIconScout} + + )}
@@ -291,7 +312,14 @@ let ButtonTmpComp = (function () { .setPropertyViewFn((children) => ( <>
- {children.prefixIcon.propertyView({ + { children.sourceMode.propertyView({ + label: "", + radioButton: true + })} + {children.sourceMode.getView() === 'standard' && children.prefixIcon.propertyView({ + label: trans("button.icon"), + })} + {children.sourceMode.getView() === 'advanced' &&children.prefixIconScout.propertyView({ label: trans("button.icon"), })}
diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx new file mode 100644 index 000000000..47cca644f --- /dev/null +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -0,0 +1,611 @@ +import type { EditorState, EditorView } from "base/codeEditor/codeMirror"; +import { iconRegexp, iconWidgetClass } from "base/codeEditor/extensions/iconExtension"; +import { i18nObjs, trans } from "i18n"; +import { + AbstractComp, + CompAction, + CompActionTypes, + CompParams, + customAction, + DispatchType, + Node, + SimpleComp, + ValueAndMsg, +} from "lowcoder-core"; +import { + BlockGrayLabel, + controlItem, + ControlPropertyViewWrapper, + DeleteInputIcon, + iconPrefix, + IconSelect, + IconSelectBase, + removeQuote, + SwitchJsIcon, + SwitchWrapper, + TacoButton, + TacoInput, + useIcon, + wrapperToControlItem, +} from "lowcoder-design"; +import { ReactNode, SetStateAction, useCallback, useEffect, useRef, useState } from "react"; +import styled from "styled-components"; +import { setFieldsNoTypeCheck } from "util/objectUtils"; +import { StringControl } from "./codeControl"; +import { ControlParams } from "./controlParams"; +import Popover from "antd/es/popover"; +import { CloseIcon, SearchIcon } from "icons"; +import Draggable from "react-draggable"; +import IconscoutApi, { SearchParams } from "api/iconscoutApi"; +import List, { ListRowProps } from "react-virtualized/dist/es/List"; +import { debounce } from "lodash"; +import Spin from "antd/es/spin"; + +const ButtonWrapper = styled.div` + width: 100%; + display: flex; + align-items: center; +`; +const ButtonIconWrapper = styled.div` + display: flex; + width: 18px; +`; +const ButtonText = styled.div` + margin: 0 4px; + flex: 1; + width: 0px; + line-height: 20px; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; +`; +const StyledDeleteInputIcon = styled(DeleteInputIcon)` + margin-left: auto; + cursor: pointer; + + &:hover circle { + fill: #8b8fa3; + } +`; + +const StyledImage = styled.img` + height: 100%; + color: currentColor; +`; + +const Wrapper = styled.div` + > div:nth-of-type(1) { + margin-bottom: 4px; + } +`; +const PopupContainer = styled.div` + width: 580px; + background: #ffffff; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); + border-radius: 8px; + box-sizing: border-box; +`; + +const TitleDiv = styled.div` + height: 48px; + display: flex; + align-items: center; + padding: 0 16px; + justify-content: space-between; + user-select: none; +`; +const TitleText = styled.span` + font-size: 16px; + color: #222222; + line-height: 16px; +`; +const StyledCloseIcon = styled(CloseIcon)` + width: 16px; + height: 16px; + cursor: pointer; + color: #8b8fa3; + + &:hover g line { + stroke: #222222; + } +`; + +const SearchDiv = styled.div` + position: relative; + margin: 0px 16px; + padding-bottom: 8px; + display: flex; + justify-content: space-between; +`; +const StyledSearchIcon = styled(SearchIcon)` + position: absolute; + top: 6px; + left: 12px; +`; +const IconListWrapper = styled.div` + padding-left: 10px; + padding-right: 4px; +`; +const IconList = styled(List)` + scrollbar-gutter: stable; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background-clip: content-box; + border-radius: 9999px; + background-color: rgba(139, 143, 163, 0.2); + } + + &::-webkit-scrollbar-thumb:hover { + background-color: rgba(139, 143, 163, 0.36); + } +`; + +const IconRow = styled.div` + padding: 0 6px; + display: flex; + align-items: flex-start; /* Align items to the start to allow different heights */ + justify-content: space-between; + + &:last-child { + gap: 8px; + justify-content: flex-start; + } +`; + +const IconItemContainer = styled.div` + width: 60px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + cursor: pointer; + font-size: 28px; + margin-bottom: 24px; + + &:hover { + border: 1px solid #315efb; + border-radius: 4px; + } + + &:focus { + border: 1px solid #315efb; + border-radius: 4px; + box-shadow: 0 0 0 2px #d6e4ff; + } +`; + +const IconWrapper = styled.div` + height: auto; + display: flex; + align-items: center; + justify-content: center; +`; + +const IconKeyDisplay = styled.div` + font-size: 8px; + color: #8b8fa3; + margin-top: 4px; /* Space between the icon and the text */ + text-align: center; + word-wrap: break-word; /* Ensure text wraps */ + width: 100%; /* Ensure the container can grow */ +`; + +export enum IconScoutAssetType { + ICON = "icon", + ILLUSTRATION = "illustration", + // '3D' = "3d", + LOTTIE = "lottie", +} + +const IconScoutSearchParams: SearchParams = { + query: '', + product_type: 'item', + asset: 'icon', + per_page: 50, + page: 1, + formats: 'svg', + sort: 'relevant', +}; + +const columnNum = 8; + +export const IconPicker = (props: { + assetType: string; + value: string; + onChange: (value: string) => void; + label?: ReactNode; + IconType?: "OnlyAntd" | "All" | "default" | undefined; +}) => { + console.log(props.value, props.assetType); + const icon = useIcon(props.value); + const [ visible, setVisible ] = useState(false) + const [ loading, setLoading ] = useState(false) + const [searchText, setSearchText] = useState(""); + const [ searchResults, setSearchResults ] = useState>([]); + const onChangeRef = useRef(props.onChange); + onChangeRef.current = props.onChange; + const onChangeIcon = useCallback( + (key: string) => { + onChangeRef.current(key); + setVisible(false); + }, [] + ); + + const fetchResults = async (query: string) => { + console.log('query change', query); + setLoading(true); + const result = await IconscoutApi.search({ + ...IconScoutSearchParams, + asset: props.assetType, + query, + }); + setLoading(false); + setSearchResults(result.data); + }; + + const fetchAsset = async (uuid: string) => { + try { + const result = await IconscoutApi.download(uuid, { + format: props.assetType === IconScoutAssetType.LOTTIE ? 'ai' : 'svg', + }); + if (props.assetType !== IconScoutAssetType.LOTTIE) { + onChangeIcon(result.url); + } + } catch (error) { + console.error(error); + } + } + + const handleChange = debounce((e) => { + setSearchText(e.target.value); + fetchResults(e.target.value); + }, 500); + + const rowRenderer = useCallback( + (p: ListRowProps) => ( + + {searchResults + .slice(p.index * columnNum, (p.index + 1) * columnNum) + .map((icon) => ( + { + if (props.assetType === IconScoutAssetType.LOTTIE) { + onChangeIcon(icon.urls.thumb) + } + fetchAsset(icon.uuid); + }} + > + + {props.assetType === IconScoutAssetType.ICON && ( + + )} + {props.assetType === IconScoutAssetType.ILLUSTRATION && ( + + )} + {props.assetType === IconScoutAssetType.LOTTIE && ( + + + ))} + + ),[searchResults] + ); + + return ( + parent : undefined} + // hide the original background when dragging the popover is allowed + overlayInnerStyle={{ + border: "none", + boxShadow: "none", + background: "transparent", + }} + // when dragging is allowed, always re-location to avoid the popover exceeds the screen + destroyTooltipOnHide + content={ + + + + {"Select Icon"} + setVisible(false)} /> + + + + + + + {loading && ( + + )} + {!loading && ( + + )} + + + + } + > + + {props.value ? ( + + + {props.assetType === IconScoutAssetType.LOTTIE && ( + <>{props.value} + )} + {props.assetType !== IconScoutAssetType.LOTTIE && ( + + )} + + { + props.onChange(""); + e.stopPropagation(); + }} + /> + + ) : ( + + )} + + + ); +}; + +function onClickIcon(e: React.MouseEvent, v: EditorView) { + for (let t = e.target as HTMLElement | null; t; t = t.parentElement) { + if (t.classList.contains(iconWidgetClass)) { + const pos = v.posAtDOM(t); + const result = iconRegexp.exec(v.state.doc.sliceString(pos)); + if (result) { + const from = pos + result.index; + return { from, to: from + result[0].length }; + } + } + } +} + +function IconSpan(props: { value: string }) { + const icon = useIcon(props.value); + return {icon?.getView() ?? props.value}; +} + +function cardRichContent(s: string) { + let result = s.match(iconRegexp); + if (result) { + const nodes: React.ReactNode[] = []; + let pos = 0; + for (const iconStr of result) { + const i = s.indexOf(iconStr, pos); + if (i >= 0) { + nodes.push(s.slice(pos, i)); + nodes.push(); + pos = i + iconStr.length; + } + } + nodes.push(s.slice(pos)); + return nodes; + } + return s; +} + +type Range = { + from: number; + to: number; +}; + +function IconCodeEditor(props: { + codeControl: InstanceType; + params: ControlParams; +}) { + const [visible, setVisible] = useState(false); + const [range, setRange] = useState(); + const widgetPopup = useCallback( + (v: EditorView) => ( + { + const r: Range = range ?? v.state.selection.ranges[0] ?? { from: 0, to: 0 }; + const insert = '"' + value + '"'; + setRange({ ...r, to: r.from + insert.length }); + v.dispatch({ changes: { ...r, insert } }); + }} + visible={visible} + setVisible={setVisible} + trigger="contextMenu" + // parent={document.querySelector(`${CodeEditorTooltipContainer}`)} + searchKeywords={i18nObjs.iconSearchKeywords} + /> + ), + [visible, range] + ); + const onClick = useCallback((e: React.MouseEvent, v: EditorView) => { + const r = onClickIcon(e, v); + if (r) { + setVisible(true); + setRange(r); + } + }, []); + const extraOnChange = useCallback((state: EditorState) => { + // popover should hide on change + setVisible(false); + setRange(undefined); + }, []); + return props.codeControl.codeEditor({ + ...props.params, + enableIcon: true, + widgetPopup, + onClick, + extraOnChange, + cardRichContent, + cardTips: ( + <> + {trans("iconControl.insertImage")} + setVisible(true)}> + {trans("iconControl.insertIcon")} + + + ), + }); +} + +function isSelectValue(value: any) { + return !value || (typeof value === "string" && value.startsWith(iconPrefix)); +} + +type ChangeModeAction = { + useCodeEditor: boolean; +}; + +export function IconControlView(props: { value: string }) { + const { value } = props; + const icon = useIcon(value); + console.log(icon); + if (icon) { + return icon.getView(); + } + return ; +} + +export function IconscoutControl( + assetType: string = IconScoutAssetType.ICON, +) { + return class extends AbstractComp>> { + private readonly useCodeEditor: boolean; + private readonly codeControl: InstanceType; + + constructor(params: CompParams) { + super(params); + this.useCodeEditor = !isSelectValue(params.value); + this.codeControl = new StringControl(params); + } + + override getView(): ReactNode { + const value = this.codeControl.getView(); + return ; + } + + override getPropertyView(): ReactNode { + throw new Error("Method not implemented."); + } + + changeModeAction() { + return customAction({ useCodeEditor: !this.useCodeEditor }, true); + } + + propertyView(params: ControlParams) { + return wrapperToControlItem( + + this.dispatchChangeValueAction(x)} + label={params.label} + IconType={params.IconType} + /> + + ); + } + + readonly IGNORABLE_DEFAULT_VALUE = ""; + override toJsonValue(): string { + if (this.useCodeEditor) { + return this.codeControl.toJsonValue(); + } + // in select mode, don't save editor's original value when saving + const v = removeQuote(this.codeControl.getView()); + return isSelectValue(v) ? v : ""; + } + + override reduce(action: CompAction): this { + switch (action.type) { + case CompActionTypes.CUSTOM: { + const useCodeEditor = (action.value as ChangeModeAction).useCodeEditor; + let codeControl = this.codeControl; + if (!this.useCodeEditor && useCodeEditor) { + // value should be transformed when switching to editor from select mode + const value = this.codeControl.toJsonValue(); + if (value && isSelectValue(value)) { + codeControl = codeControl.reduce(codeControl.changeValueAction(`{{ "${value}" }}`)); + } + } + return setFieldsNoTypeCheck(this, { useCodeEditor, codeControl }); + } + case CompActionTypes.CHANGE_VALUE: { + const useCodeEditor = this.useCodeEditor ? true : !isSelectValue(action.value); + const codeControl = this.codeControl.reduce(action); + if (useCodeEditor !== this.useCodeEditor || codeControl !== this.codeControl) { + return setFieldsNoTypeCheck(this, { useCodeEditor, codeControl }); + } + return this; + } + } + const codeControl = this.codeControl.reduce(action); + if (codeControl !== this.codeControl) { + return setFieldsNoTypeCheck(this, { codeControl }); + } + return this; + } + + override nodeWithoutCache() { + return this.codeControl.nodeWithoutCache(); + } + + exposingNode() { + return this.codeControl.exposingNode(); + } + + override changeDispatch(dispatch: DispatchType): this { + const result = setFieldsNoTypeCheck( + super.changeDispatch(dispatch), + { codeControl: this.codeControl.changeDispatch(dispatch) }, + { keepCacheKeys: ["node"] } + ); + return result; + } + } +} + +// export class IconscoutControl extends SimpleComp { +// readonly IGNORABLE_DEFAULT_VALUE = false; +// protected getDefaultValue(): string { +// return ''; +// } + +// override getPropertyView(): ReactNode { +// throw new Error("Method not implemented."); +// } + +// propertyView(params: ControlParams & { type?: "switch" | "checkbox" }) { +// return wrapperToControlItem( +// +// this.dispatchChangeValueAction(x)} +// label={params.label} +// IconType={params.IconType} +// /> +// +// ); +// } +// } From 6879f3b16f378efa36ae3afe2bd4240f2ee1e3ed Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Wed, 3 Jul 2024 12:36:02 +0500 Subject: [PATCH 02/18] integrate iconscout with jsonLottie comp --- .../packages/lowcoder/src/api/iconscoutApi.ts | 9 +- .../lowcoder/src/comps/comps/imageComp.tsx | 4 +- .../comps/comps/jsonComp/jsonLottieComp.tsx | 44 ++- .../comps/comps/meetingComp/controlButton.tsx | 2 +- .../src/comps/controls/iconscoutControl.tsx | 275 +++--------------- 5 files changed, 84 insertions(+), 250 deletions(-) diff --git a/client/packages/lowcoder/src/api/iconscoutApi.ts b/client/packages/lowcoder/src/api/iconscoutApi.ts index 8f947d88f..a41c213e6 100644 --- a/client/packages/lowcoder/src/api/iconscoutApi.ts +++ b/client/packages/lowcoder/src/api/iconscoutApi.ts @@ -17,8 +17,8 @@ export type ResponseType = { }; const apiUrl = "https://api.iconscout.com"; -const clientID = ""; //"91870410585071"; -const clientSecret = ""; // "GV5aCWpwdLWTxVXFBjMKSoyDPUyjzXLR"; +const clientID = ""; +const clientSecret = ""; const currentPage = 1; const currentQuery = ''; const currentData = []; @@ -75,6 +75,11 @@ class IconscoutApi extends Api { }); return response?.data.response.download; } + + static async downloadJSON(url: string): Promise { + const response = await axios.get(url) + return response?.data; + } } export default IconscoutApi; diff --git a/client/packages/lowcoder/src/comps/comps/imageComp.tsx b/client/packages/lowcoder/src/comps/comps/imageComp.tsx index 7456fc2ff..ccd330a36 100644 --- a/client/packages/lowcoder/src/comps/comps/imageComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/imageComp.tsx @@ -202,7 +202,7 @@ const ContainerImg = (props: RecordConstructorToView) => { { {children.sourceMode.getView() === 'standard' && children.src.propertyView({ label: trans("image.src"), })} - {children.sourceMode.getView() === 'advanced' &&children.srcIconScout.propertyView({ + {children.sourceMode.getView() === 'advanced' && children.srcIconScout.propertyView({ label: trans("image.src"), })} diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index cfaf06bca..e1c664b1a 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx @@ -2,6 +2,7 @@ import { hiddenPropertyView, showDataLoadingIndicatorsPropertyView } from "comps import { ArrayOrJSONObjectControl, NumberControl, + StringControl, } from "comps/controls/codeControl"; import { dropdownControl } from "comps/controls/dropdownControl"; import { BoolControl } from "comps/controls/boolControl"; @@ -19,6 +20,9 @@ import { import { defaultLottie } from "./jsonConstants"; import { EditorContext } from "comps/editorState"; import { IconScoutAssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; +import { isEmpty } from "lodash"; +import IconscoutApi from "@lowcoder-ee/api/iconscoutApi"; +import { changeValueAction, multiChangeAction } from "lowcoder-core"; const Player = lazy( () => import('@lottiefiles/react-lottie-player') @@ -98,7 +102,8 @@ let JsonLottieTmpComp = (function () { JSON.stringify(defaultLottie, null, 2) ), srcIconScout: IconscoutControl(IconScoutAssetType.LOTTIE), - valueIconScout: ArrayOrJSONObjectControl, + uuidIconScout: StringControl, + valueIconScout: withDefault(ArrayOrJSONObjectControl, JSON.stringify({})), speed: dropdownControl(speedOptions, "1"), width: withDefault(NumberControl, 100), height: withDefault(NumberControl, 100), @@ -108,11 +113,38 @@ let JsonLottieTmpComp = (function () { loop: dropdownControl(loopOptions, "single"), keepLastFrame: BoolControl.DEFAULT_TRUE, }; - return new UICompBuilder(childrenMap, (props) => { - console.log(props.srcIconScout); + return new UICompBuilder(childrenMap, (props, dispatch) => { + + const downloadAsset = async (uuid: string) => { + try { + const result = await IconscoutApi.download(uuid, { + format: 'ai', + }); + if (result && result.download_url) { + const json = await IconscoutApi.downloadJSON(result.download_url); + dispatch( + multiChangeAction({ + uuidIconScout: changeValueAction(uuid, true), + valueIconScout: changeValueAction(JSON.stringify(json, null, 2), true) + }) + ) + } + } catch(error) { + console.error(error); + } + + } + useEffect(() => { + if(props.srcIconScout?.uuid && props.srcIconScout?.uuid !== props.uuidIconScout) { + // get asset download link + downloadAsset(props.srcIconScout?.uuid); + } + }, [props.srcIconScout]); + return (
- {props.prefixIconScout} + )} diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index 47cca644f..b9c731903 100644 --- a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -1,38 +1,18 @@ -import type { EditorState, EditorView } from "base/codeEditor/codeMirror"; -import { iconRegexp, iconWidgetClass } from "base/codeEditor/extensions/iconExtension"; -import { i18nObjs, trans } from "i18n"; +import { trans } from "i18n"; import { - AbstractComp, - CompAction, - CompActionTypes, - CompParams, - customAction, - DispatchType, - Node, SimpleComp, - ValueAndMsg, } from "lowcoder-core"; import { BlockGrayLabel, - controlItem, ControlPropertyViewWrapper, DeleteInputIcon, - iconPrefix, - IconSelect, - IconSelectBase, - removeQuote, - SwitchJsIcon, - SwitchWrapper, TacoButton, TacoInput, useIcon, wrapperToControlItem, } from "lowcoder-design"; -import { ReactNode, SetStateAction, useCallback, useEffect, useRef, useState } from "react"; +import { ReactNode, useCallback, useRef, useState } from "react"; import styled from "styled-components"; -import { setFieldsNoTypeCheck } from "util/objectUtils"; -import { StringControl } from "./codeControl"; -import { ControlParams } from "./controlParams"; import Popover from "antd/es/popover"; import { CloseIcon, SearchIcon } from "icons"; import Draggable from "react-draggable"; @@ -40,6 +20,7 @@ import IconscoutApi, { SearchParams } from "api/iconscoutApi"; import List, { ListRowProps } from "react-virtualized/dist/es/List"; import { debounce } from "lodash"; import Spin from "antd/es/spin"; +import { ControlParams } from "./controlParams"; const ButtonWrapper = styled.div` width: 100%; @@ -215,8 +196,9 @@ const columnNum = 8; export const IconPicker = (props: { assetType: string; + uuid: string; value: string; - onChange: (value: string) => void; + onChange: (key: string, value: string) => void; label?: ReactNode; IconType?: "OnlyAntd" | "All" | "default" | undefined; }) => { @@ -229,8 +211,8 @@ export const IconPicker = (props: { const onChangeRef = useRef(props.onChange); onChangeRef.current = props.onChange; const onChangeIcon = useCallback( - (key: string) => { - onChangeRef.current(key); + (key: string, value: string) => { + onChangeRef.current(key, value); setVisible(false); }, [] ); @@ -250,11 +232,9 @@ export const IconPicker = (props: { const fetchAsset = async (uuid: string) => { try { const result = await IconscoutApi.download(uuid, { - format: props.assetType === IconScoutAssetType.LOTTIE ? 'ai' : 'svg', + format: 'svg', }); - if (props.assetType !== IconScoutAssetType.LOTTIE) { - onChangeIcon(result.url); - } + onChangeIcon(result.uuid, result.url); } catch (error) { console.error(error); } @@ -276,9 +256,10 @@ export const IconPicker = (props: { tabIndex={0} onClick={() => { if (props.assetType === IconScoutAssetType.LOTTIE) { - onChangeIcon(icon.urls.thumb) + onChangeIcon(icon.uuid, icon.urls.thumb ) + } else { + fetchAsset(icon.uuid); } - fetchAsset(icon.uuid); }} > @@ -352,15 +333,15 @@ export const IconPicker = (props: { {props.assetType === IconScoutAssetType.LOTTIE && ( - <>{props.value} + { - props.onChange(""); + props.onChange("", ""); e.stopPropagation(); }} /> @@ -373,239 +354,51 @@ export const IconPicker = (props: { ); }; -function onClickIcon(e: React.MouseEvent, v: EditorView) { - for (let t = e.target as HTMLElement | null; t; t = t.parentElement) { - if (t.classList.contains(iconWidgetClass)) { - const pos = v.posAtDOM(t); - const result = iconRegexp.exec(v.state.doc.sliceString(pos)); - if (result) { - const from = pos + result.index; - return { from, to: from + result[0].length }; - } - } - } -} - -function IconSpan(props: { value: string }) { - const icon = useIcon(props.value); - return {icon?.getView() ?? props.value}; -} - -function cardRichContent(s: string) { - let result = s.match(iconRegexp); - if (result) { - const nodes: React.ReactNode[] = []; - let pos = 0; - for (const iconStr of result) { - const i = s.indexOf(iconStr, pos); - if (i >= 0) { - nodes.push(s.slice(pos, i)); - nodes.push(); - pos = i + iconStr.length; - } - } - nodes.push(s.slice(pos)); - return nodes; - } - return s; -} - -type Range = { - from: number; - to: number; -}; - -function IconCodeEditor(props: { - codeControl: InstanceType; - params: ControlParams; -}) { - const [visible, setVisible] = useState(false); - const [range, setRange] = useState(); - const widgetPopup = useCallback( - (v: EditorView) => ( - { - const r: Range = range ?? v.state.selection.ranges[0] ?? { from: 0, to: 0 }; - const insert = '"' + value + '"'; - setRange({ ...r, to: r.from + insert.length }); - v.dispatch({ changes: { ...r, insert } }); - }} - visible={visible} - setVisible={setVisible} - trigger="contextMenu" - // parent={document.querySelector(`${CodeEditorTooltipContainer}`)} - searchKeywords={i18nObjs.iconSearchKeywords} - /> - ), - [visible, range] - ); - const onClick = useCallback((e: React.MouseEvent, v: EditorView) => { - const r = onClickIcon(e, v); - if (r) { - setVisible(true); - setRange(r); - } - }, []); - const extraOnChange = useCallback((state: EditorState) => { - // popover should hide on change - setVisible(false); - setRange(undefined); - }, []); - return props.codeControl.codeEditor({ - ...props.params, - enableIcon: true, - widgetPopup, - onClick, - extraOnChange, - cardRichContent, - cardTips: ( - <> - {trans("iconControl.insertImage")} - setVisible(true)}> - {trans("iconControl.insertIcon")} - - - ), - }); -} - -function isSelectValue(value: any) { - return !value || (typeof value === "string" && value.startsWith(iconPrefix)); -} - -type ChangeModeAction = { - useCodeEditor: boolean; -}; - -export function IconControlView(props: { value: string }) { +export function IconControlView(props: { value: string, uuid: string }) { const { value } = props; const icon = useIcon(value); - console.log(icon); + if (icon) { return icon.getView(); } return ; } +type DataType = { + uuid: string; + value: string; +} export function IconscoutControl( assetType: string = IconScoutAssetType.ICON, ) { - return class extends AbstractComp>> { - private readonly useCodeEditor: boolean; - private readonly codeControl: InstanceType; - - constructor(params: CompParams) { - super(params); - this.useCodeEditor = !isSelectValue(params.value); - this.codeControl = new StringControl(params); - } - - override getView(): ReactNode { - const value = this.codeControl.getView(); - return ; + return class IconscoutControl extends SimpleComp { + readonly IGNORABLE_DEFAULT_VALUE = false; + protected getDefaultValue(): DataType { + return { + uuid: '', + value: '', + }; } override getPropertyView(): ReactNode { throw new Error("Method not implemented."); } - changeModeAction() { - return customAction({ useCodeEditor: !this.useCodeEditor }, true); - } - - propertyView(params: ControlParams) { + propertyView(params: ControlParams & { type?: "switch" | "checkbox" }) { return wrapperToControlItem( this.dispatchChangeValueAction(x)} + uuid={this.value.uuid} + value={this.value.value} + onChange={(uuid, value) => { + this.dispatchChangeValueAction({uuid, value}) + }} label={params.label} IconType={params.IconType} /> ); } - - readonly IGNORABLE_DEFAULT_VALUE = ""; - override toJsonValue(): string { - if (this.useCodeEditor) { - return this.codeControl.toJsonValue(); - } - // in select mode, don't save editor's original value when saving - const v = removeQuote(this.codeControl.getView()); - return isSelectValue(v) ? v : ""; - } - - override reduce(action: CompAction): this { - switch (action.type) { - case CompActionTypes.CUSTOM: { - const useCodeEditor = (action.value as ChangeModeAction).useCodeEditor; - let codeControl = this.codeControl; - if (!this.useCodeEditor && useCodeEditor) { - // value should be transformed when switching to editor from select mode - const value = this.codeControl.toJsonValue(); - if (value && isSelectValue(value)) { - codeControl = codeControl.reduce(codeControl.changeValueAction(`{{ "${value}" }}`)); - } - } - return setFieldsNoTypeCheck(this, { useCodeEditor, codeControl }); - } - case CompActionTypes.CHANGE_VALUE: { - const useCodeEditor = this.useCodeEditor ? true : !isSelectValue(action.value); - const codeControl = this.codeControl.reduce(action); - if (useCodeEditor !== this.useCodeEditor || codeControl !== this.codeControl) { - return setFieldsNoTypeCheck(this, { useCodeEditor, codeControl }); - } - return this; - } - } - const codeControl = this.codeControl.reduce(action); - if (codeControl !== this.codeControl) { - return setFieldsNoTypeCheck(this, { codeControl }); - } - return this; - } - - override nodeWithoutCache() { - return this.codeControl.nodeWithoutCache(); - } - - exposingNode() { - return this.codeControl.exposingNode(); - } - - override changeDispatch(dispatch: DispatchType): this { - const result = setFieldsNoTypeCheck( - super.changeDispatch(dispatch), - { codeControl: this.codeControl.changeDispatch(dispatch) }, - { keepCacheKeys: ["node"] } - ); - return result; - } } } - -// export class IconscoutControl extends SimpleComp { -// readonly IGNORABLE_DEFAULT_VALUE = false; -// protected getDefaultValue(): string { -// return ''; -// } - -// override getPropertyView(): ReactNode { -// throw new Error("Method not implemented."); -// } - -// propertyView(params: ControlParams & { type?: "switch" | "checkbox" }) { -// return wrapperToControlItem( -// -// this.dispatchChangeValueAction(x)} -// label={params.label} -// IconType={params.IconType} -// /> -// -// ); -// } -// } From 65111f9d0c67ea807f9f62a7d39b2d894694b575 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Fri, 21 Feb 2025 00:08:56 +0500 Subject: [PATCH 03/18] added dotlottie option in lottie comp --- client/packages/lowcoder/package.json | 1 + .../comps/comps/jsonComp/jsonLottieComp.tsx | 79 +++++++++++++------ client/yarn.lock | 19 +++++ 3 files changed, 76 insertions(+), 23 deletions(-) diff --git a/client/packages/lowcoder/package.json b/client/packages/lowcoder/package.json index 9338fa428..4c4689031 100644 --- a/client/packages/lowcoder/package.json +++ b/client/packages/lowcoder/package.json @@ -24,6 +24,7 @@ "@fortawesome/free-regular-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "latest", + "@lottiefiles/dotlottie-react": "^0.13.0", "@manaflair/redux-batch": "^1.0.0", "@rjsf/antd": "^5.21.2", "@rjsf/core": "^5.21.2", diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index e1c664b1a..4a32deb2c 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx @@ -29,6 +29,11 @@ const Player = lazy( .then(module => ({default: module.Player})) ); +const DotLottiePlayer = lazy( + () => import('@lottiefiles/dotlottie-react') + .then(module => ({default: module.DotLottieReact})) +); + /** * JsonLottie Comp */ @@ -90,8 +95,9 @@ const speedOptions = [ ] as const; const ModeOptions = [ - { label: "Data", value: "standard" }, - { label: "Advanced", value: "advanced" }, + { label: "Lottie JSON", value: "standard" }, + { label: "DotLottie", value: "dotLottie" }, + { label: "IconScout", value: "advanced" }, ] as const; let JsonLottieTmpComp = (function () { @@ -102,6 +108,7 @@ let JsonLottieTmpComp = (function () { JSON.stringify(defaultLottie, null, 2) ), srcIconScout: IconscoutControl(IconScoutAssetType.LOTTIE), + srcDotLottie: withDefault(StringControl, 'https://assets-v2.lottiefiles.com/a/9e7d8a50-1180-11ee-89a6-3b0ab1ca8a0e/hUfEwc6xNt.lottie'), uuidIconScout: StringControl, valueIconScout: withDefault(ArrayOrJSONObjectControl, JSON.stringify({})), speed: dropdownControl(speedOptions, "1"), @@ -162,27 +169,50 @@ let JsonLottieTmpComp = (function () { rotate: props.container.rotation, }} > - + {props.sourceMode === 'dotLottie' + ? ( + + ) + : ( + + ) + }
); @@ -198,6 +228,9 @@ let JsonLottieTmpComp = (function () { {children.sourceMode.getView() === 'standard' && children.value.propertyView({ label: trans("jsonLottie.lottieJson"), })} + {children.sourceMode.getView() === 'dotLottie' && children.srcDotLottie.propertyView({ + label: "Source", + })} {children.sourceMode.getView() === 'advanced' && children.srcIconScout.propertyView({ label: "Lottie Source", })} diff --git a/client/yarn.lock b/client/yarn.lock index 7fae135fa..b819a2716 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -3113,6 +3113,24 @@ __metadata: languageName: node linkType: hard +"@lottiefiles/dotlottie-react@npm:^0.13.0": + version: 0.13.0 + resolution: "@lottiefiles/dotlottie-react@npm:0.13.0" + dependencies: + "@lottiefiles/dotlottie-web": 0.40.1 + peerDependencies: + react: ^17 || ^18 || ^19 + checksum: bafe6ded727aab991ff03f6ff5a2fd1a41b1f429b36175f34140017fc684e0a8ef7f7b713d189bd49948c4b728fe1d05c7d8c20a0bea0d8c1ae1ed87614fe843 + languageName: node + linkType: hard + +"@lottiefiles/dotlottie-web@npm:0.40.1": + version: 0.40.1 + resolution: "@lottiefiles/dotlottie-web@npm:0.40.1" + checksum: a79e60c33845311cb055ea661abb2f4211063e149788aea724afbed05a09ae569d50b4c0e5825d13eb5fc62a33c3dc74f2f3900fdb1e99f8594feddc72d2cc27 + languageName: node + linkType: hard + "@lottiefiles/react-lottie-player@npm:^3.5.3": version: 3.5.3 resolution: "@lottiefiles/react-lottie-player@npm:3.5.3" @@ -14232,6 +14250,7 @@ coolshapes-react@lowcoder-org/coolshapes-react: "@fortawesome/free-regular-svg-icons": ^6.5.1 "@fortawesome/free-solid-svg-icons": ^6.5.1 "@fortawesome/react-fontawesome": latest + "@lottiefiles/dotlottie-react": ^0.13.0 "@manaflair/redux-batch": ^1.0.0 "@rjsf/antd": ^5.21.2 "@rjsf/core": ^5.21.2 From 9dfe557a29a5389657ad02fc1baa9b8c370dfcbf Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Mon, 24 Feb 2025 23:56:27 +0500 Subject: [PATCH 04/18] download asset from icon-scout and save as base64 string --- .../packages/lowcoder/src/api/iconscoutApi.ts | 7 +- .../src/comps/controls/iconscoutControl.tsx | 113 +++++++++++------- 2 files changed, 73 insertions(+), 47 deletions(-) diff --git a/client/packages/lowcoder/src/api/iconscoutApi.ts b/client/packages/lowcoder/src/api/iconscoutApi.ts index a41c213e6..0d44a7ef2 100644 --- a/client/packages/lowcoder/src/api/iconscoutApi.ts +++ b/client/packages/lowcoder/src/api/iconscoutApi.ts @@ -68,16 +68,15 @@ class IconscoutApi extends Api { static async download(uuid: string, params: Record): Promise { const response = await getAxiosInstance(clientSecret).request({ - url: `/v3/items/${uuid}/api-download`, + url: `/v3/items/${uuid}/api-download?format=${params.format}`, method: "POST", withCredentials: false, - params, }); return response?.data.response.download; } - static async downloadJSON(url: string): Promise { - const response = await axios.get(url) + static async downloadAsset(url: string): Promise { + const response = await axios.get(url, {responseType: 'blob'}) return response?.data; } } diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index b9c731903..7c356624a 100644 --- a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -11,7 +11,7 @@ import { useIcon, wrapperToControlItem, } from "lowcoder-design"; -import { ReactNode, useCallback, useRef, useState } from "react"; +import { ReactNode, useCallback, useMemo, useRef, useState } from "react"; import styled from "styled-components"; import Popover from "antd/es/popover"; import { CloseIcon, SearchIcon } from "icons"; @@ -21,6 +21,7 @@ import List, { ListRowProps } from "react-virtualized/dist/es/List"; import { debounce } from "lodash"; import Spin from "antd/es/spin"; import { ControlParams } from "./controlParams"; +import { getBase64 } from "@lowcoder-ee/util/fileUtils"; const ButtonWrapper = styled.div` width: 100%; @@ -175,13 +176,19 @@ const IconKeyDisplay = styled.div` width: 100%; /* Ensure the container can grow */ `; -export enum IconScoutAssetType { +export enum AssetType { ICON = "icon", ILLUSTRATION = "illustration", // '3D' = "3d", LOTTIE = "lottie", } +export type IconScoutAsset = { + uuid: string; + value: string; + preview: string; +} + const IconScoutSearchParams: SearchParams = { query: '', product_type: 'item', @@ -198,27 +205,26 @@ export const IconPicker = (props: { assetType: string; uuid: string; value: string; - onChange: (key: string, value: string) => void; + preview: string; + onChange: (key: string, value: string, preview: string) => void; label?: ReactNode; IconType?: "OnlyAntd" | "All" | "default" | undefined; }) => { - console.log(props.value, props.assetType); - const icon = useIcon(props.value); const [ visible, setVisible ] = useState(false) const [ loading, setLoading ] = useState(false) - const [searchText, setSearchText] = useState(""); const [ searchResults, setSearchResults ] = useState>([]); + const onChangeRef = useRef(props.onChange); onChangeRef.current = props.onChange; + const onChangeIcon = useCallback( - (key: string, value: string) => { - onChangeRef.current(key, value); + (key: string, value: string, url: string) => { + onChangeRef.current(key, value, url); setVisible(false); }, [] ); const fetchResults = async (query: string) => { - console.log('query change', query); setLoading(true); const result = await IconscoutApi.search({ ...IconScoutSearchParams, @@ -229,19 +235,38 @@ export const IconPicker = (props: { setSearchResults(result.data); }; - const fetchAsset = async (uuid: string) => { + const downloadAsset = async ( + uuid: string, + downloadUrl: string, + callback: (assetUrl: string) => void, + ) => { + try { + if (uuid && downloadUrl) { + const json = await IconscoutApi.downloadAsset(downloadUrl); + getBase64(json, (url: string) => { + callback(url); + }); + } + } catch(error) { + console.error(error); + } + } + + const fetchDownloadUrl = async (uuid: string) => { try { const result = await IconscoutApi.download(uuid, { - format: 'svg', + format: props.assetType === AssetType.LOTTIE ? 'lottie' : 'svg', + }); + + downloadAsset(uuid, result.download_url, (assetUrl: string) => { + onChangeIcon(uuid, assetUrl, result.url); }); - onChangeIcon(result.uuid, result.url); } catch (error) { console.error(error); } } const handleChange = debounce((e) => { - setSearchText(e.target.value); fetchResults(e.target.value); }, 500); @@ -255,21 +280,17 @@ export const IconPicker = (props: { key={icon.uuid} tabIndex={0} onClick={() => { - if (props.assetType === IconScoutAssetType.LOTTIE) { - onChangeIcon(icon.uuid, icon.urls.thumb ) - } else { - fetchAsset(icon.uuid); - } + fetchDownloadUrl(icon.uuid); }} > - {props.assetType === IconScoutAssetType.ICON && ( + {props.assetType === AssetType.ICON && ( )} - {props.assetType === IconScoutAssetType.ILLUSTRATION && ( + {props.assetType === AssetType.ILLUSTRATION && ( )} - {props.assetType === IconScoutAssetType.LOTTIE && ( + {props.assetType === AssetType.LOTTIE && ( @@ -279,6 +300,12 @@ export const IconPicker = (props: { ),[searchResults] ); + const popupTitle = useMemo(() => { + if (props.assetType === AssetType.ILLUSTRATION) return 'Search Image'; + if (props.assetType === AssetType.LOTTIE) return 'Search Animation'; + return 'Search Icon'; + }, [props.assetType]); + return ( parent : undefined} // hide the original background when dragging the popover is allowed - overlayInnerStyle={{ - border: "none", - boxShadow: "none", - background: "transparent", - }} // when dragging is allowed, always re-location to avoid the popover exceeds the screen + styles={{ + body: { + border: "none", + boxShadow: "none", + background: "transparent", + } + }} destroyTooltipOnHide content={ - {"Select Icon"} + {popupTitle} setVisible(false)} /> @@ -329,19 +358,19 @@ export const IconPicker = (props: { } > - {props.value ? ( + {props.preview ? ( - {props.assetType === IconScoutAssetType.LOTTIE && ( - { - props.onChange("", ""); + props.onChange("", "", ""); e.stopPropagation(); }} /> @@ -364,19 +393,16 @@ export function IconControlView(props: { value: string, uuid: string }) { return ; } -type DataType = { - uuid: string; - value: string; -} export function IconscoutControl( - assetType: string = IconScoutAssetType.ICON, + assetType: string = AssetType.ICON, ) { - return class IconscoutControl extends SimpleComp { + return class IconscoutControl extends SimpleComp { readonly IGNORABLE_DEFAULT_VALUE = false; - protected getDefaultValue(): DataType { + protected getDefaultValue(): IconScoutAsset { return { uuid: '', value: '', + preview: '', }; } @@ -391,8 +417,9 @@ export function IconscoutControl( assetType={assetType} uuid={this.value.uuid} value={this.value.value} - onChange={(uuid, value) => { - this.dispatchChangeValueAction({uuid, value}) + preview={this.value.preview} + onChange={(uuid, value, preview) => { + this.dispatchChangeValueAction({uuid, value, preview}) }} label={params.label} IconType={params.IconType} From 7ee413587594d0d714600689c59dbb1e468bc8a0 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Mon, 24 Feb 2025 23:57:32 +0500 Subject: [PATCH 05/18] updated naming for iconScoutAsset --- .../packages/lowcoder/src/comps/comps/imageComp.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/imageComp.tsx b/client/packages/lowcoder/src/comps/comps/imageComp.tsx index ccd330a36..1806399e2 100644 --- a/client/packages/lowcoder/src/comps/comps/imageComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/imageComp.tsx @@ -36,7 +36,7 @@ import { EditorContext } from "comps/editorState"; import { StringControl } from "../controls/codeControl"; import { PositionControl } from "comps/controls/dropdownControl"; import { dropdownControl } from "../controls/dropdownControl"; -import { IconScoutAssetType, IconscoutControl } from "../controls/iconscoutControl"; +import { AssetType, IconscoutControl } from "../controls/iconscoutControl"; const Container = styled.div<{ $style: ImageStyleType | undefined, @@ -115,7 +115,7 @@ const getStyle = (style: ImageStyleType) => { const EventOptions = [clickEvent] as const; const ModeOptions = [ { label: "URL", value: "standard" }, - { label: "Advanced", value: "advanced" }, + { label: "Asset Library", value: "asset-library" }, ] as const; const ContainerImg = (props: RecordConstructorToView) => { @@ -201,8 +201,8 @@ const ContainerImg = (props: RecordConstructorToView) => { > ) => { const childrenMap = { sourceMode: dropdownControl(ModeOptions, "standard"), src: withDefault(StringStateControl, "https://temp.im/350x400"), - srcIconScout: IconscoutControl(IconScoutAssetType.ILLUSTRATION), + iconScoutAsset: IconscoutControl(AssetType.ILLUSTRATION), onEvent: eventHandlerControl(EventOptions), style: styleControl(ImageStyle , 'style'), animationStyle: styleControl(AnimationStyle , 'animationStyle'), @@ -253,7 +253,7 @@ let ImageBasicComp = new UICompBuilder(childrenMap, (props) => { {children.sourceMode.getView() === 'standard' && children.src.propertyView({ label: trans("image.src"), })} - {children.sourceMode.getView() === 'advanced' && children.srcIconScout.propertyView({ + {children.sourceMode.getView() === 'asset-library' && children.iconScoutAsset.propertyView({ label: trans("image.src"), })} From fdc8050e8f026d0d819b226f11d2ec8a0967a103 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Mon, 24 Feb 2025 23:57:43 +0500 Subject: [PATCH 06/18] updated naming for iconScoutAsset --- .../src/comps/comps/meetingComp/controlButton.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx index 2239f3a39..ec3f0a216 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx @@ -39,7 +39,7 @@ import { useEffect, useRef, useState } from "react"; import ReactResizeDetector from "react-resize-detector"; import { useContext } from "react"; -import { IconScoutAssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; +import { AssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; const Container = styled.div<{ $style: any }>` height: 100%; @@ -173,7 +173,7 @@ const typeOptions = [ const ModeOptions = [ { label: "Standard", value: "standard" }, - { label: "Advanced", value: "advanced" }, + { label: "Asset Library", value: "asset-library" }, ] as const; function isDefault(type?: string) { @@ -198,7 +198,7 @@ const childrenMap = { form: SelectFormControl, sourceMode: dropdownControl(ModeOptions, "standard"), prefixIcon: IconControl, - prefixIconScout: IconscoutControl(IconScoutAssetType.ICON), + iconScoutAsset: IconscoutControl(AssetType.ICON), style: ButtonStyleControl, viewRef: RefControl, restrictPaddingOnRotation:withDefault(StringControl, 'controlButton') @@ -241,7 +241,7 @@ let ButtonTmpComp = (function () { setStyle(container?.clientHeight + "px", container?.clientWidth + "px"); }; - console.log(props.prefixIconScout); + return ( {(editorState) => ( @@ -292,11 +292,11 @@ let ButtonTmpComp = (function () { {props.prefixIcon} )} - {props.sourceMode === 'advanced' && props.prefixIconScout && ( + {props.sourceMode === 'asset-library' && props.iconScoutAsset && ( - + )} @@ -319,7 +319,7 @@ let ButtonTmpComp = (function () { {children.sourceMode.getView() === 'standard' && children.prefixIcon.propertyView({ label: trans("button.icon"), })} - {children.sourceMode.getView() === 'advanced' &&children.prefixIconScout.propertyView({ + {children.sourceMode.getView() === 'asset-library' &&children.iconScoutAsset.propertyView({ label: trans("button.icon"), })} From b29b1dd4288a15e21b25a1a94b263805aa3ce29f Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Mon, 24 Feb 2025 23:58:37 +0500 Subject: [PATCH 07/18] used dotLottie player for different modes --- .../comps/comps/jsonComp/jsonLottieComp.tsx | 135 ++++++------------ 1 file changed, 44 insertions(+), 91 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index 4a32deb2c..08d0b08fd 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx @@ -2,7 +2,6 @@ import { hiddenPropertyView, showDataLoadingIndicatorsPropertyView } from "comps import { ArrayOrJSONObjectControl, NumberControl, - StringControl, } from "comps/controls/codeControl"; import { dropdownControl } from "comps/controls/dropdownControl"; import { BoolControl } from "comps/controls/boolControl"; @@ -10,7 +9,7 @@ import { styleControl } from "comps/controls/styleControl"; import { AnimationStyle, LottieStyle } from "comps/controls/styleControlConstants"; import { trans } from "i18n"; import { Section, sectionNames } from "lowcoder-design"; -import { useContext, lazy, useEffect } from "react"; +import { useContext, lazy, useEffect, useState } from "react"; import { UICompBuilder, withDefault } from "../../generators"; import { NameConfig, @@ -19,15 +18,13 @@ import { } from "../../generators/withExposing"; import { defaultLottie } from "./jsonConstants"; import { EditorContext } from "comps/editorState"; -import { IconScoutAssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; -import { isEmpty } from "lodash"; -import IconscoutApi from "@lowcoder-ee/api/iconscoutApi"; -import { changeValueAction, multiChangeAction } from "lowcoder-core"; +import { AssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; +import { DotLottie } from "@lottiefiles/dotlottie-react"; -const Player = lazy( - () => import('@lottiefiles/react-lottie-player') - .then(module => ({default: module.Player})) -); +// const Player = lazy( +// () => import('@lottiefiles/react-lottie-player') +// .then(module => ({default: module.Player})) +// ); const DotLottiePlayer = lazy( () => import('@lottiefiles/dotlottie-react') @@ -44,7 +41,7 @@ const animationStartOptions = [ }, { label: trans("jsonLottie.onHover"), - value: "on hover", + value: "hover", }, ] as const; @@ -96,8 +93,7 @@ const speedOptions = [ const ModeOptions = [ { label: "Lottie JSON", value: "standard" }, - { label: "DotLottie", value: "dotLottie" }, - { label: "IconScout", value: "advanced" }, + { label: "Asset Library", value: "asset-library" } ] as const; let JsonLottieTmpComp = (function () { @@ -107,10 +103,7 @@ let JsonLottieTmpComp = (function () { ArrayOrJSONObjectControl, JSON.stringify(defaultLottie, null, 2) ), - srcIconScout: IconscoutControl(IconScoutAssetType.LOTTIE), - srcDotLottie: withDefault(StringControl, 'https://assets-v2.lottiefiles.com/a/9e7d8a50-1180-11ee-89a6-3b0ab1ca8a0e/hUfEwc6xNt.lottie'), - uuidIconScout: StringControl, - valueIconScout: withDefault(ArrayOrJSONObjectControl, JSON.stringify({})), + iconScoutAsset: IconscoutControl(AssetType.LOTTIE), speed: dropdownControl(speedOptions, "1"), width: withDefault(NumberControl, 100), height: withDefault(NumberControl, 100), @@ -121,32 +114,23 @@ let JsonLottieTmpComp = (function () { keepLastFrame: BoolControl.DEFAULT_TRUE, }; return new UICompBuilder(childrenMap, (props, dispatch) => { - - const downloadAsset = async (uuid: string) => { - try { - const result = await IconscoutApi.download(uuid, { - format: 'ai', - }); - if (result && result.download_url) { - const json = await IconscoutApi.downloadJSON(result.download_url); - dispatch( - multiChangeAction({ - uuidIconScout: changeValueAction(uuid, true), - valueIconScout: changeValueAction(JSON.stringify(json, null, 2), true) - }) - ) - } - } catch(error) { - console.error(error); + const [dotLottie, setDotLottie] = useState(null); + + useEffect(() => { + const onComplete = () => { + props.keepLastFrame && dotLottie?.setFrame(100); } - } - useEffect(() => { - if(props.srcIconScout?.uuid && props.srcIconScout?.uuid !== props.uuidIconScout) { - // get asset download link - downloadAsset(props.srcIconScout?.uuid); + if (dotLottie) { + dotLottie.addEventListener('complete', onComplete); } - }, [props.srcIconScout]); + + return () => { + if (dotLottie) { + dotLottie.removeEventListener('complete', onComplete); + } + }; + }, [dotLottie, props.keepLastFrame]); return (
- {props.sourceMode === 'dotLottie' - ? ( - - ) - : ( - - ) - } + : undefined} + src={props.sourceMode === 'asset-library' ? props.iconScoutAsset?.value : undefined} + style={{ + height: "100%", + width: "100%", + maxWidth: "100%", + maxHeight: "100%", + }} + onMouseEnter={() => props.animationStart === "hover" && dotLottie?.play()} + onMouseLeave={() => props.animationStart === "hover" && dotLottie?.pause()} + />
); @@ -228,15 +187,9 @@ let JsonLottieTmpComp = (function () { {children.sourceMode.getView() === 'standard' && children.value.propertyView({ label: trans("jsonLottie.lottieJson"), })} - {children.sourceMode.getView() === 'dotLottie' && children.srcDotLottie.propertyView({ - label: "Source", - })} - {children.sourceMode.getView() === 'advanced' && children.srcIconScout.propertyView({ + {children.sourceMode.getView() === 'asset-library' && children.iconScoutAsset.propertyView({ label: "Lottie Source", })} - {children.sourceMode.getView() === 'advanced' && children.valueIconScout.propertyView({ - label: trans("jsonLottie.lottieJson"), - })} {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( From 09d7e9d389b2227899c0e93cb87148b6069de3bd Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Mon, 24 Feb 2025 23:59:05 +0500 Subject: [PATCH 08/18] added option in IconComp to select icons from IconScout --- .../lowcoder/src/comps/comps/iconComp.tsx | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/iconComp.tsx b/client/packages/lowcoder/src/comps/comps/iconComp.tsx index f93b0eb2a..9a8eb1790 100644 --- a/client/packages/lowcoder/src/comps/comps/iconComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/iconComp.tsx @@ -30,6 +30,8 @@ import { } from "../controls/eventHandlerControl"; import { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { AssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; +import { dropdownControl } from "../controls/dropdownControl"; const Container = styled.div<{ $style: IconStyleType | undefined; @@ -61,10 +63,17 @@ const Container = styled.div<{ const EventOptions = [clickEvent] as const; +const ModeOptions = [ + { label: "Standard", value: "standard" }, + { label: "Asset Library", value: "asset-library" }, +] as const; + const childrenMap = { style: styleControl(IconStyle,'style'), animationStyle: styleControl(AnimationStyle,'animationStyle'), + sourceMode: dropdownControl(ModeOptions, "standard"), icon: withDefault(IconControl, "/icon:antd/homefilled"), + iconScoutAsset: IconscoutControl(AssetType.ICON), autoHeight: withDefault(AutoHeightControl, "auto"), iconSize: withDefault(NumberControl, 20), onEvent: eventHandlerControl(EventOptions), @@ -103,7 +112,10 @@ const IconView = (props: RecordConstructorToView) => { }} onClick={() => props.onEvent("click")} > - {props.icon} + { props.sourceMode === 'standard' + ? props.icon + : + } )} > @@ -117,11 +129,17 @@ let IconBasicComp = (function () { .setPropertyViewFn((children) => ( <>
- {children.icon.propertyView({ + { children.sourceMode.propertyView({ + label: "", + radioButton: true + })} + {children.sourceMode.getView() === 'standard' && children.icon.propertyView({ label: trans("iconComp.icon"), IconType: "All", })} - + {children.sourceMode.getView() === 'asset-library' && children.iconScoutAsset.propertyView({ + label: trans("button.icon"), + })}
{["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( From 60e456181d08306b341ef796148dfc287e35bf14 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Tue, 25 Feb 2025 17:52:05 +0500 Subject: [PATCH 09/18] fixed asset selection popup --- .../src/comps/controls/iconscoutControl.tsx | 65 +++++++++---------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index 7c356624a..5f72b8f2d 100644 --- a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -22,6 +22,9 @@ import { debounce } from "lodash"; import Spin from "antd/es/spin"; import { ControlParams } from "./controlParams"; import { getBase64 } from "@lowcoder-ee/util/fileUtils"; +import Flex from "antd/es/flex"; +import Typography from "antd/es/typography"; +import LoadingOutlined from "@ant-design/icons/LoadingOutlined"; const ButtonWrapper = styled.div` width: 100%; @@ -32,15 +35,7 @@ const ButtonIconWrapper = styled.div` display: flex; width: 18px; `; -const ButtonText = styled.div` - margin: 0 4px; - flex: 1; - width: 0px; - line-height: 20px; - overflow: hidden; - text-overflow: ellipsis; - text-align: left; -`; + const StyledDeleteInputIcon = styled(DeleteInputIcon)` margin-left: auto; cursor: pointer; @@ -61,7 +56,10 @@ const Wrapper = styled.div` } `; const PopupContainer = styled.div` + display: flex; + flex-direction: column; width: 580px; + min-height: 480px; background: #ffffff; box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); border-radius: 8px; @@ -167,15 +165,6 @@ const IconWrapper = styled.div` justify-content: center; `; -const IconKeyDisplay = styled.div` - font-size: 8px; - color: #8b8fa3; - margin-top: 4px; /* Space between the icon and the text */ - text-align: center; - word-wrap: break-word; /* Ensure text wraps */ - width: 100%; /* Ensure the container can grow */ -`; - export enum AssetType { ICON = "icon", ILLUSTRATION = "illustration", @@ -212,6 +201,7 @@ export const IconPicker = (props: { }) => { const [ visible, setVisible ] = useState(false) const [ loading, setLoading ] = useState(false) + const [ searchText, setSearchText ] = useState('') const [ searchResults, setSearchResults ] = useState>([]); const onChangeRef = useRef(props.onChange); @@ -252,14 +242,14 @@ export const IconPicker = (props: { } } - const fetchDownloadUrl = async (uuid: string) => { + const fetchDownloadUrl = async (uuid: string, preview: string) => { try { const result = await IconscoutApi.download(uuid, { format: props.assetType === AssetType.LOTTIE ? 'lottie' : 'svg', }); downloadAsset(uuid, result.download_url, (assetUrl: string) => { - onChangeIcon(uuid, assetUrl, result.url); + onChangeIcon(uuid, assetUrl, preview); }); } catch (error) { console.error(error); @@ -268,6 +258,7 @@ export const IconPicker = (props: { const handleChange = debounce((e) => { fetchResults(e.target.value); + setSearchText(e.target.value); }, 500); const rowRenderer = useCallback( @@ -280,7 +271,10 @@ export const IconPicker = (props: { key={icon.uuid} tabIndex={0} onClick={() => { - fetchDownloadUrl(icon.uuid); + fetchDownloadUrl( + icon.uuid, + props.assetType === AssetType.ICON ? icon.urls.png_64 : icon.urls.thumb, + ); }} > @@ -310,12 +304,8 @@ export const IconPicker = (props: { parent : undefined} - // hide the original background when dragging the popover is allowed - // when dragging is allowed, always re-location to avoid the popover exceeds the screen styles={{ body: { border: "none", @@ -339,11 +329,20 @@ export const IconPicker = (props: { /> - - {loading && ( - - )} - {!loading && ( + {loading && ( + + } /> + + )} + {!loading && Boolean(searchText) && !searchResults?.length && ( + + + No results found. + + + )} + {!loading && Boolean(searchText) && searchResults?.length && ( + - )} - + + )}
} @@ -365,7 +364,7 @@ export const IconPicker = (props: {