From 0639f1bc80bb27edf30fc3a1da8cee571e9fb62f Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> 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 0000000000..8f947d88fb --- /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<string, string> = { + "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<any> { + 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<string, string>): Promise<any> { + 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 d78a21d201..7456fc2ff5 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<typeof childrenMap>) => { const imgRef = useRef<HTMLDivElement>(null); @@ -194,7 +200,11 @@ const ContainerImg = (props: RecordConstructorToView<typeof childrenMap>) => { } > <AntImage - src={props.src.value} + src={ + props.sourceMode === 'advanced' + ? (props.srcIconScout as ReactElement)?.props.value + : props.src.value + } referrerPolicy="same-origin" draggable={false} preview={props.supportPreview ? {src: props.previewSrc || props.src.value } : false} @@ -210,7 +220,9 @@ const ContainerImg = (props: RecordConstructorToView<typeof childrenMap>) => { }; 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 ( <> <Section name={sectionNames.basic}> - {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"), })} </Section> diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index c3f93b6e1c..cfaf06bca9 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 ( <div style={{ @@ -145,7 +155,17 @@ let JsonLottieTmpComp = (function () { return ( <> <Section name={sectionNames.basic}> - {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"), })} </Section> diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx index 28e318618c..f8edb43d61 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<HTMLElement>, restrictPaddingOnRotation:withDefault(StringControl, 'controlButton') @@ -226,7 +241,7 @@ let ButtonTmpComp = (function () { setStyle(container?.clientHeight + "px", container?.clientWidth + "px"); }; - + console.log(props.prefixIconScout); return ( <EditorContext.Consumer> {(editorState) => ( @@ -270,14 +285,20 @@ let ButtonTmpComp = (function () { : submitForm(editorState, props.form) } > - {props.prefixIcon && ( + {props.sourceMode === 'standard' && props.prefixIcon && ( <IconWrapper $style={{ ...props.style, size: props.iconSize }} > {props.prefixIcon} </IconWrapper> )} - + {props.sourceMode === 'advanced' && props.prefixIconScout && ( + <IconScoutWrapper + $style={{ ...props.style, size: props.iconSize }} + > + {props.prefixIconScout} + </IconScoutWrapper> + )} </Button100> </div> </Container> @@ -291,7 +312,14 @@ let ButtonTmpComp = (function () { .setPropertyViewFn((children) => ( <> <Section name={sectionNames.basic}> - {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"), })} </Section> 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 0000000000..47cca644ff --- /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<Array<any>>([]); + 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) => ( + <IconRow key={p.key} style={p.style}> + {searchResults + .slice(p.index * columnNum, (p.index + 1) * columnNum) + .map((icon) => ( + <IconItemContainer + key={icon.uuid} + tabIndex={0} + onClick={() => { + if (props.assetType === IconScoutAssetType.LOTTIE) { + onChangeIcon(icon.urls.thumb) + } + fetchAsset(icon.uuid); + }} + > + <IconWrapper> + {props.assetType === IconScoutAssetType.ICON && ( + <img style={{'width': '100%'}} src={icon.urls.png_64} /> + )} + {props.assetType === IconScoutAssetType.ILLUSTRATION && ( + <img style={{'width': '100%'}} src={icon.urls.thumb} /> + )} + {props.assetType === IconScoutAssetType.LOTTIE && ( + <video style={{'width': '100%'}} src={icon.urls.thumb} autoPlay /> + )} + </IconWrapper> + </IconItemContainer> + ))} + </IconRow> + ),[searchResults] + ); + + return ( + <Popover + trigger={'click'} + placement="left" + // align={{ offset: [props.leftOffset ?? 0, 0, 0, 0] }} + open={visible} + onOpenChange={setVisible} + // getPopupContainer={parent ? () => 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={ + <Draggable handle=".dragHandle"> + <PopupContainer> + <TitleDiv className="dragHandle"> + <TitleText>{"Select Icon"}</TitleText> + <StyledCloseIcon onClick={() => setVisible(false)} /> + </TitleDiv> + <SearchDiv> + <TacoInput + style={{ width: "100%", paddingLeft: "40px" }} + onChange={handleChange} + placeholder={"Search Icon"} + /> + <StyledSearchIcon /> + </SearchDiv> + <IconListWrapper> + {loading && ( + <Spin /> + )} + {!loading && ( + <IconList + width={550} + height={400} + rowHeight={80} + rowCount={Math.ceil(searchResults.length / columnNum)} + rowRenderer={rowRenderer} + /> + )} + </IconListWrapper> + </PopupContainer> + </Draggable> + } + > + <TacoButton style={{ width: "100%" }}> + {props.value ? ( + <ButtonWrapper> + <ButtonIconWrapper> + {props.assetType === IconScoutAssetType.LOTTIE && ( + <>{props.value}</> + )} + {props.assetType !== IconScoutAssetType.LOTTIE && ( + <IconControlView value={props.value} /> + )} + </ButtonIconWrapper> + <StyledDeleteInputIcon + onClick={(e) => { + props.onChange(""); + e.stopPropagation(); + }} + /> + </ButtonWrapper> + ) : ( + <BlockGrayLabel label={trans("iconControl.selectIcon")} /> + )} + </TacoButton> + </Popover> + ); +}; + +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 <span>{icon?.getView() ?? props.value}</span>; +} + +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(<IconSpan key={i} value={iconStr} />); + pos = i + iconStr.length; + } + } + nodes.push(s.slice(pos)); + return nodes; + } + return s; +} + +type Range = { + from: number; + to: number; +}; + +function IconCodeEditor(props: { + codeControl: InstanceType<typeof StringControl>; + params: ControlParams; +}) { + const [visible, setVisible] = useState(false); + const [range, setRange] = useState<Range>(); + const widgetPopup = useCallback( + (v: EditorView) => ( + <IconSelectBase + onChange={(value) => { + 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<HTMLElement>(`${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")} + <TacoButton style={{ display: "inline" }} onClick={() => setVisible(true)}> + {trans("iconControl.insertIcon")} + </TacoButton> + </> + ), + }); +} + +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 <StyledImage src={value} alt="" />; +} + +export function IconscoutControl( + assetType: string = IconScoutAssetType.ICON, +) { + return class extends AbstractComp<ReactNode, string, Node<ValueAndMsg<string>>> { + private readonly useCodeEditor: boolean; + private readonly codeControl: InstanceType<typeof StringControl>; + + constructor(params: CompParams<string>) { + super(params); + this.useCodeEditor = !isSelectValue(params.value); + this.codeControl = new StringControl(params); + } + + override getView(): ReactNode { + const value = this.codeControl.getView(); + return <IconControlView value={value} />; + } + + override getPropertyView(): ReactNode { + throw new Error("Method not implemented."); + } + + changeModeAction() { + return customAction<ChangeModeAction>({ useCodeEditor: !this.useCodeEditor }, true); + } + + propertyView(params: ControlParams) { + return wrapperToControlItem( + <ControlPropertyViewWrapper {...params}> + <IconPicker + assetType={assetType} + value={this.codeControl.getView()} + onChange={(x) => this.dispatchChangeValueAction(x)} + label={params.label} + IconType={params.IconType} + /> + </ControlPropertyViewWrapper> + ); + } + + 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<string> { +// 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( +// <ControlPropertyViewWrapper {...params}> +// <IconPicker +// value={this.value} +// onChange={(x) => this.dispatchChangeValueAction(x)} +// label={params.label} +// IconType={params.IconType} +// /> +// </ControlPropertyViewWrapper> +// ); +// } +// } From 6879f3b16f378efa36ae3afe2bd4240f2ee1e3ed Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> 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 8f947d88fb..a41c213e66 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<any> { + 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 7456fc2ff5..ccd330a365 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<typeof childrenMap>) => { <AntImage src={ props.sourceMode === 'advanced' - ? (props.srcIconScout as ReactElement)?.props.value + ? props.srcIconScout?.value : props.src.value } referrerPolicy="same-origin" @@ -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() === 'advanced' && children.srcIconScout.propertyView({ label: trans("image.src"), })} </Section> diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index cfaf06bca9..e1c664b1a0 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 ( <div style={{ + height: '100%', padding: `${props.container.margin}`, animation: props.animationStyle.animation, animationDelay: props.animationStyle.animationDelay, @@ -139,7 +171,11 @@ let JsonLottieTmpComp = (function () { hover={props.animationStart === "on hover" && true} loop={props.loop === "single" ? false : true} speed={Number(props.speed)} - src={props.value} + src={ + props.sourceMode === 'advanced' + ? (isEmpty(props.valueIconScout) ? '' : props.valueIconScout) + : props.value + } style={{ height: "100%", width: "100%", diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx index f8edb43d61..2239f3a39b 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx @@ -296,7 +296,7 @@ let ButtonTmpComp = (function () { <IconScoutWrapper $style={{ ...props.style, size: props.iconSize }} > - {props.prefixIconScout} + <img src={props.prefixIconScout.value} /> </IconScoutWrapper> )} </Button100> diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index 47cca644ff..b9c731903d 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); }} > <IconWrapper> @@ -352,15 +333,15 @@ export const IconPicker = (props: { <ButtonWrapper> <ButtonIconWrapper> {props.assetType === IconScoutAssetType.LOTTIE && ( - <>{props.value}</> + <video style={{'width': '100%'}} src={props.value} autoPlay /> )} {props.assetType !== IconScoutAssetType.LOTTIE && ( - <IconControlView value={props.value} /> + <IconControlView value={props.value} uuid={props.uuid}/> )} </ButtonIconWrapper> <StyledDeleteInputIcon onClick={(e) => { - 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 <span>{icon?.getView() ?? props.value}</span>; -} - -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(<IconSpan key={i} value={iconStr} />); - pos = i + iconStr.length; - } - } - nodes.push(s.slice(pos)); - return nodes; - } - return s; -} - -type Range = { - from: number; - to: number; -}; - -function IconCodeEditor(props: { - codeControl: InstanceType<typeof StringControl>; - params: ControlParams; -}) { - const [visible, setVisible] = useState(false); - const [range, setRange] = useState<Range>(); - const widgetPopup = useCallback( - (v: EditorView) => ( - <IconSelectBase - onChange={(value) => { - 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<HTMLElement>(`${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")} - <TacoButton style={{ display: "inline" }} onClick={() => setVisible(true)}> - {trans("iconControl.insertIcon")} - </TacoButton> - </> - ), - }); -} - -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 <StyledImage src={value} alt="" />; } +type DataType = { + uuid: string; + value: string; +} export function IconscoutControl( assetType: string = IconScoutAssetType.ICON, ) { - return class extends AbstractComp<ReactNode, string, Node<ValueAndMsg<string>>> { - private readonly useCodeEditor: boolean; - private readonly codeControl: InstanceType<typeof StringControl>; - - constructor(params: CompParams<string>) { - super(params); - this.useCodeEditor = !isSelectValue(params.value); - this.codeControl = new StringControl(params); - } - - override getView(): ReactNode { - const value = this.codeControl.getView(); - return <IconControlView value={value} />; + return class IconscoutControl extends SimpleComp<DataType> { + readonly IGNORABLE_DEFAULT_VALUE = false; + protected getDefaultValue(): DataType { + return { + uuid: '', + value: '', + }; } override getPropertyView(): ReactNode { throw new Error("Method not implemented."); } - changeModeAction() { - return customAction<ChangeModeAction>({ useCodeEditor: !this.useCodeEditor }, true); - } - - propertyView(params: ControlParams) { + propertyView(params: ControlParams & { type?: "switch" | "checkbox" }) { return wrapperToControlItem( <ControlPropertyViewWrapper {...params}> <IconPicker assetType={assetType} - value={this.codeControl.getView()} - onChange={(x) => this.dispatchChangeValueAction(x)} + uuid={this.value.uuid} + value={this.value.value} + onChange={(uuid, value) => { + this.dispatchChangeValueAction({uuid, value}) + }} label={params.label} IconType={params.IconType} /> </ControlPropertyViewWrapper> ); } - - 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<string> { -// 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( -// <ControlPropertyViewWrapper {...params}> -// <IconPicker -// value={this.value} -// onChange={(x) => this.dispatchChangeValueAction(x)} -// label={params.label} -// IconType={params.IconType} -// /> -// </ControlPropertyViewWrapper> -// ); -// } -// } From 65111f9d0c67ea807f9f62a7d39b2d894694b575 Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> 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 9338fa428d..4c4689031f 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 e1c664b1a0..4a32deb2c2 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, }} > - <Player - key={ - [props.speed, props.animationStart, props.loop, props.value, props.keepLastFrame] as any - } - keepLastFrame={props.keepLastFrame} - autoplay={props.animationStart === "auto" && true} - hover={props.animationStart === "on hover" && true} - loop={props.loop === "single" ? false : true} - speed={Number(props.speed)} - src={ - props.sourceMode === 'advanced' - ? (isEmpty(props.valueIconScout) ? '' : props.valueIconScout) - : props.value - } - style={{ - height: "100%", - width: "100%", - maxWidth: "100%", - maxHeight: "100%", - }} - /> + {props.sourceMode === 'dotLottie' + ? ( + <DotLottiePlayer + key={ + [props.speed, props.animationStart, props.loop, props.value, props.keepLastFrame] as any + } + // keepLastFrame={props.keepLastFrame} + autoplay={props.animationStart === "auto" && true} + playOnHover={props.animationStart === "on hover" && true} + loop={props.loop === "single" ? false : true} + speed={Number(props.speed)} + src={props.srcDotLottie} + style={{ + height: "100%", + width: "100%", + maxWidth: "100%", + maxHeight: "100%", + }} + /> + ) + : ( + <Player + key={ + [props.speed, props.animationStart, props.loop, props.value, props.keepLastFrame] as any + } + keepLastFrame={props.keepLastFrame} + autoplay={props.animationStart === "auto" && true} + hover={props.animationStart === "on hover" && true} + loop={props.loop === "single" ? false : true} + speed={Number(props.speed)} + src={ + props.sourceMode === 'advanced' + ? (isEmpty(props.valueIconScout) ? '' : props.valueIconScout) + : props.value + } + style={{ + height: "100%", + width: "100%", + maxWidth: "100%", + maxHeight: "100%", + }} + /> + ) + } </div> </div> ); @@ -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 7fae135fa1..b819a27167 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 <mraheeliftikhar1994@gmail.com> 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 a41c213e66..0d44a7ef23 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<string, string>): Promise<any> { 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<any> { - const response = await axios.get(url) + static async downloadAsset(url: string): Promise<any> { + 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 b9c731903d..7c356624a9 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<Array<any>>([]); + 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); }} > <IconWrapper> - {props.assetType === IconScoutAssetType.ICON && ( + {props.assetType === AssetType.ICON && ( <img style={{'width': '100%'}} src={icon.urls.png_64} /> )} - {props.assetType === IconScoutAssetType.ILLUSTRATION && ( + {props.assetType === AssetType.ILLUSTRATION && ( <img style={{'width': '100%'}} src={icon.urls.thumb} /> )} - {props.assetType === IconScoutAssetType.LOTTIE && ( + {props.assetType === AssetType.LOTTIE && ( <video style={{'width': '100%'}} src={icon.urls.thumb} autoPlay /> )} </IconWrapper> @@ -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 ( <Popover trigger={'click'} @@ -288,25 +315,27 @@ export const IconPicker = (props: { onOpenChange={setVisible} // getPopupContainer={parent ? () => 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={ <Draggable handle=".dragHandle"> <PopupContainer> <TitleDiv className="dragHandle"> - <TitleText>{"Select Icon"}</TitleText> + <TitleText>{popupTitle}</TitleText> <StyledCloseIcon onClick={() => setVisible(false)} /> </TitleDiv> <SearchDiv> <TacoInput style={{ width: "100%", paddingLeft: "40px" }} onChange={handleChange} - placeholder={"Search Icon"} + placeholder={popupTitle} /> <StyledSearchIcon /> </SearchDiv> @@ -329,19 +358,19 @@ export const IconPicker = (props: { } > <TacoButton style={{ width: "100%" }}> - {props.value ? ( + {props.preview ? ( <ButtonWrapper> <ButtonIconWrapper> - {props.assetType === IconScoutAssetType.LOTTIE && ( - <video style={{'width': '100%'}} src={props.value} autoPlay /> + {props.assetType === AssetType.LOTTIE && ( + <video style={{'width': '100%'}} src={props.preview} autoPlay /> )} - {props.assetType !== IconScoutAssetType.LOTTIE && ( - <IconControlView value={props.value} uuid={props.uuid}/> + {props.assetType !== AssetType.LOTTIE && ( + <IconControlView value={props.preview} uuid={props.uuid}/> )} </ButtonIconWrapper> <StyledDeleteInputIcon onClick={(e) => { - props.onChange("", ""); + props.onChange("", "", ""); e.stopPropagation(); }} /> @@ -364,19 +393,16 @@ export function IconControlView(props: { value: string, uuid: string }) { return <StyledImage src={value} alt="" />; } -type DataType = { - uuid: string; - value: string; -} export function IconscoutControl( - assetType: string = IconScoutAssetType.ICON, + assetType: string = AssetType.ICON, ) { - return class IconscoutControl extends SimpleComp<DataType> { + return class IconscoutControl extends SimpleComp<IconScoutAsset> { 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 <mraheeliftikhar1994@gmail.com> 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 ccd330a365..1806399e26 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<typeof childrenMap>) => { @@ -201,8 +201,8 @@ const ContainerImg = (props: RecordConstructorToView<typeof childrenMap>) => { > <AntImage src={ - props.sourceMode === 'advanced' - ? props.srcIconScout?.value + props.sourceMode === 'asset-library' + ? props.iconScoutAsset?.value : props.src.value } referrerPolicy="same-origin" @@ -222,7 +222,7 @@ const ContainerImg = (props: RecordConstructorToView<typeof childrenMap>) => { 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"), })} </Section> From fdc8050e8f026d0d819b226f11d2ec8a0967a103 Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> 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 2239f3a39b..ec3f0a216a 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<HTMLElement>, restrictPaddingOnRotation:withDefault(StringControl, 'controlButton') @@ -241,7 +241,7 @@ let ButtonTmpComp = (function () { setStyle(container?.clientHeight + "px", container?.clientWidth + "px"); }; - console.log(props.prefixIconScout); + return ( <EditorContext.Consumer> {(editorState) => ( @@ -292,11 +292,11 @@ let ButtonTmpComp = (function () { {props.prefixIcon} </IconWrapper> )} - {props.sourceMode === 'advanced' && props.prefixIconScout && ( + {props.sourceMode === 'asset-library' && props.iconScoutAsset && ( <IconScoutWrapper $style={{ ...props.style, size: props.iconSize }} > - <img src={props.prefixIconScout.value} /> + <img src={props.iconScoutAsset.value} /> </IconScoutWrapper> )} </Button100> @@ -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"), })} </Section> From b29b1dd4288a15e21b25a1a94b263805aa3ce29f Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> 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 4a32deb2c2..08d0b08fd3 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<DotLottie | null>(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 ( <div @@ -169,50 +153,25 @@ let JsonLottieTmpComp = (function () { rotate: props.container.rotation, }} > - {props.sourceMode === 'dotLottie' - ? ( - <DotLottiePlayer - key={ - [props.speed, props.animationStart, props.loop, props.value, props.keepLastFrame] as any - } - // keepLastFrame={props.keepLastFrame} - autoplay={props.animationStart === "auto" && true} - playOnHover={props.animationStart === "on hover" && true} - loop={props.loop === "single" ? false : true} - speed={Number(props.speed)} - src={props.srcDotLottie} - style={{ - height: "100%", - width: "100%", - maxWidth: "100%", - maxHeight: "100%", - }} - /> - ) - : ( - <Player - key={ - [props.speed, props.animationStart, props.loop, props.value, props.keepLastFrame] as any - } - keepLastFrame={props.keepLastFrame} - autoplay={props.animationStart === "auto" && true} - hover={props.animationStart === "on hover" && true} - loop={props.loop === "single" ? false : true} - speed={Number(props.speed)} - src={ - props.sourceMode === 'advanced' - ? (isEmpty(props.valueIconScout) ? '' : props.valueIconScout) - : props.value - } - style={{ - height: "100%", - width: "100%", - maxWidth: "100%", - maxHeight: "100%", - }} - /> - ) - } + <DotLottiePlayer + key={ + [props.speed, props.animationStart, props.loop, props.value, props.keepLastFrame] as any + } + dotLottieRefCallback={setDotLottie} + autoplay={props.animationStart === "auto"} + loop={props.loop === "single" ? false : true} + speed={Number(props.speed)} + data={props.sourceMode === 'standard' ? props.value as Record<string, undefined> : 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()} + /> </div> </div> ); @@ -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"), - })} </Section> {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( From 09d7e9d389b2227899c0e93cb87148b6069de3bd Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> 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 f93b0eb2a3..9a8eb17907 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<typeof childrenMap>) => { }} onClick={() => props.onEvent("click")} > - {props.icon} + { props.sourceMode === 'standard' + ? props.icon + : <img src={props.iconScoutAsset.value} /> + } </Container> )} > @@ -117,11 +129,17 @@ let IconBasicComp = (function () { .setPropertyViewFn((children) => ( <> <Section name={sectionNames.basic}> - {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"), + })} </Section> {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( From 60e456181d08306b341ef796148dfc287e35bf14 Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> 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 7c356624a9..5f72b8f2d9 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<string>('') const [ searchResults, setSearchResults ] = useState<Array<any>>([]); 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, + ); }} > <IconWrapper> @@ -310,12 +304,8 @@ export const IconPicker = (props: { <Popover trigger={'click'} placement="left" - // align={{ offset: [props.leftOffset ?? 0, 0, 0, 0] }} open={visible} onOpenChange={setVisible} - // getPopupContainer={parent ? () => 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: { /> <StyledSearchIcon /> </SearchDiv> - <IconListWrapper> - {loading && ( - <Spin /> - )} - {!loading && ( + {loading && ( + <Flex align="center" justify="center" style={{flex: 1}}> + <Spin indicator={<LoadingOutlined style={{ fontSize: 25 }} spin />} /> + </Flex> + )} + {!loading && Boolean(searchText) && !searchResults?.length && ( + <Flex align="center" justify="center" style={{flex: 1}}> + <Typography.Text type="secondary"> + No results found. + </Typography.Text> + </Flex> + )} + {!loading && Boolean(searchText) && searchResults?.length && ( + <IconListWrapper> <IconList width={550} height={400} @@ -351,8 +350,8 @@ export const IconPicker = (props: { rowCount={Math.ceil(searchResults.length / columnNum)} rowRenderer={rowRenderer} /> - )} - </IconListWrapper> + </IconListWrapper> + )} </PopupContainer> </Draggable> } @@ -365,7 +364,7 @@ export const IconPicker = (props: { <video style={{'width': '100%'}} src={props.preview} autoPlay /> )} {props.assetType !== AssetType.LOTTIE && ( - <IconControlView value={props.preview} uuid={props.uuid}/> + <IconControlView value={props.value} uuid={props.uuid}/> )} </ButtonIconWrapper> <StyledDeleteInputIcon From 5a73042562ce6d075ddbd92fa8bdb260d9b8adad Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> Date: Tue, 25 Feb 2025 22:16:24 +0500 Subject: [PATCH 10/18] show free/premium assets + redirect to subscription page on clicking premium asset --- .../packages/lowcoder/src/api/iconscoutApi.ts | 4 +- .../comps/comps/meetingComp/controlButton.tsx | 6 +- .../src/comps/controls/iconscoutControl.tsx | 101 +++++++++++++----- .../packages/lowcoder/src/i18n/locales/en.ts | 9 ++ 4 files changed, 89 insertions(+), 31 deletions(-) diff --git a/client/packages/lowcoder/src/api/iconscoutApi.ts b/client/packages/lowcoder/src/api/iconscoutApi.ts index 0d44a7ef23..71ea45adee 100644 --- a/client/packages/lowcoder/src/api/iconscoutApi.ts +++ b/client/packages/lowcoder/src/api/iconscoutApi.ts @@ -9,7 +9,8 @@ export interface SearchParams { per_page: number; page: 1; sort: string; - formats: string; + formats?: string; + price?: string; } export type ResponseType = { @@ -57,7 +58,6 @@ class IconscoutApi extends Api { withCredentials: false, params: { ...params, - 'formats[]': params.formats, }, }); } catch (error) { diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx index ec3f0a216a..e1d1158de0 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx @@ -342,13 +342,13 @@ let ButtonTmpComp = (function () { {children.iconSize.propertyView({ label: trans("button.iconSize"), })} - </Section> - <Section name={sectionNames.style}> - {children.style.getPropertyView()} {children.aspectRatio.propertyView({ label: trans("style.aspectRatio"), })} </Section> + <Section name={sectionNames.style}> + {children.style.getPropertyView()} + </Section> </> )} </> diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index 5f72b8f2d9..9f60e66f3f 100644 --- a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -5,6 +5,7 @@ import { import { BlockGrayLabel, ControlPropertyViewWrapper, + CustomModal, DeleteInputIcon, TacoButton, TacoInput, @@ -25,6 +26,9 @@ 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"; +import Badge from "antd/es/badge"; +import { CrownFilled } from "@ant-design/icons"; +import { SUBSCRIPTION_SETTING } from "@lowcoder-ee/constants/routesURL"; const ButtonWrapper = styled.div` width: 100%; @@ -125,7 +129,7 @@ const IconList = styled(List)` `; const IconRow = styled.div` - padding: 0 6px; + padding: 6px; display: flex; align-items: flex-start; /* Align items to the start to allow different heights */ justify-content: space-between; @@ -134,37 +138,56 @@ const IconRow = styled.div` gap: 8px; justify-content: flex-start; } + + .ant-badge { + height: 100%; + } `; const IconItemContainer = styled.div` width: 60px; + height: 60px; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; cursor: pointer; font-size: 28px; - margin-bottom: 24px; + border-radius: 4px; + background: #fafafa; &:hover { - border: 1px solid #315efb; - border-radius: 4px; + box-shadow: 0 8px 24px #1a29470a,0 2px 8px #1a294714; } &:focus { border: 1px solid #315efb; - border-radius: 4px; box-shadow: 0 0 0 2px #d6e4ff; } `; -const IconWrapper = styled.div` - height: auto; +const IconWrapper = styled.div<{$isPremium?: boolean}>` + height: 100%; display: flex; align-items: center; justify-content: center; + ${props => props.$isPremium && 'opacity: 0.25' }; +`; + +const StyledPreviewIcon = styled.img` + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; `; +const StyledPreviewLotte = styled.video` + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; +` + export enum AssetType { ICON = "icon", ILLUSTRATION = "illustration", @@ -182,9 +205,8 @@ const IconScoutSearchParams: SearchParams = { query: '', product_type: 'item', asset: 'icon', - per_page: 50, + per_page: 25, page: 1, - formats: 'svg', sort: 'relevant', }; @@ -216,13 +238,20 @@ export const IconPicker = (props: { const fetchResults = async (query: string) => { setLoading(true); - const result = await IconscoutApi.search({ + const freeResult = await IconscoutApi.search({ ...IconScoutSearchParams, asset: props.assetType, + price: 'free', + query, + }); + const premiumResult = await IconscoutApi.search({ + ...IconScoutSearchParams, + asset: props.assetType, + price: 'premium', query, }); setLoading(false); - setSearchResults(result.data); + setSearchResults([...freeResult.data, ...premiumResult.data]); }; const downloadAsset = async ( @@ -271,23 +300,43 @@ export const IconPicker = (props: { key={icon.uuid} tabIndex={0} onClick={() => { + // check if premium content then show subscription popup + // TODO: if user has subscription then skip this if block + if (icon.price !== 0) { + CustomModal.confirm({ + title: trans("iconScout.buySubscriptionTitle"), + content: trans("iconScout.buySubscriptionContent"), + onConfirm: () =>{ + window.open(SUBSCRIPTION_SETTING, "_blank"); + }, + confirmBtnType: "primary", + okText: trans("iconScout.buySubscriptionButton"), + }) + return; + } + fetchDownloadUrl( icon.uuid, props.assetType === AssetType.ICON ? icon.urls.png_64 : icon.urls.thumb, ); }} > - <IconWrapper> - {props.assetType === AssetType.ICON && ( - <img style={{'width': '100%'}} src={icon.urls.png_64} /> - )} - {props.assetType === AssetType.ILLUSTRATION && ( - <img style={{'width': '100%'}} src={icon.urls.thumb} /> - )} - {props.assetType === AssetType.LOTTIE && ( - <video style={{'width': '100%'}} src={icon.urls.thumb} autoPlay /> - )} - </IconWrapper> + <Badge + count={icon.price !== 0 ? <CrownFilled style={{color: "#e7b549"}} /> : undefined} + size='small' + > + <IconWrapper $isPremium={icon.price !== 0}> + {props.assetType === AssetType.ICON && ( + <StyledPreviewIcon src={icon.urls.png_64} /> + )} + {props.assetType === AssetType.ILLUSTRATION && ( + <StyledPreviewIcon src={icon.urls.thumb} /> + )} + {props.assetType === AssetType.LOTTIE && ( + <StyledPreviewLotte src={icon.urls.thumb} autoPlay /> + )} + </IconWrapper> + </Badge> </IconItemContainer> ))} </IconRow> @@ -295,9 +344,9 @@ export const IconPicker = (props: { ); const popupTitle = useMemo(() => { - if (props.assetType === AssetType.ILLUSTRATION) return 'Search Image'; - if (props.assetType === AssetType.LOTTIE) return 'Search Animation'; - return 'Search Icon'; + if (props.assetType === AssetType.ILLUSTRATION) return trans("iconScout.searchImage"); + if (props.assetType === AssetType.LOTTIE) return trans("iconScout.searchAnimation"); + return trans("iconScout.searchIcon"); }, [props.assetType]); return ( @@ -337,7 +386,7 @@ export const IconPicker = (props: { {!loading && Boolean(searchText) && !searchResults?.length && ( <Flex align="center" justify="center" style={{flex: 1}}> <Typography.Text type="secondary"> - No results found. + {trans("iconScout.noResults")} </Typography.Text> </Flex> )} diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 735b797d4e..cfc80833f5 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -4088,6 +4088,15 @@ export const en = { discord: "https://discord.com/invite/qMG9uTmAx2", }, + iconScout: { + "searchImage": "Search Image", + "searchAnimation": "Search Animation", + "searchIcon": "Search Icon", + "noResults": "No results found.", + "buySubscriptionTitle": "Unlock Premium Assets", + "buySubscriptionContent": "This asset is exclusive to Media Package Subscribers. Subscribe to Media Package and download high-quality assets without limits!", + "buySubscriptionButton": "Subscribe Now", + } }; // const jsonString = JSON.stringify(en, null, 2); From 6dea7b92ed32ba5489eaf07e9d9dc6ef1e6e047c Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> Date: Tue, 25 Feb 2025 22:30:18 +0500 Subject: [PATCH 11/18] show loading when user select's an icon to download --- .../src/comps/controls/iconscoutControl.tsx | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index 9f60e66f3f..003e3453b3 100644 --- a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -223,6 +223,7 @@ export const IconPicker = (props: { }) => { const [ visible, setVisible ] = useState(false) const [ loading, setLoading ] = useState(false) + const [ downloading, setDownloading ] = useState(false) const [ searchText, setSearchText ] = useState<string>('') const [ searchResults, setSearchResults ] = useState<Array<any>>([]); @@ -268,20 +269,24 @@ export const IconPicker = (props: { } } catch(error) { console.error(error); + setDownloading(false); } } const fetchDownloadUrl = async (uuid: string, preview: string) => { try { + setDownloading(true); const result = await IconscoutApi.download(uuid, { format: props.assetType === AssetType.LOTTIE ? 'lottie' : 'svg', }); downloadAsset(uuid, result.download_url, (assetUrl: string) => { + setDownloading(false); onChangeIcon(uuid, assetUrl, preview); }); } catch (error) { console.error(error); + setDownloading(false); } } @@ -383,24 +388,26 @@ export const IconPicker = (props: { <Spin indicator={<LoadingOutlined style={{ fontSize: 25 }} spin />} /> </Flex> )} - {!loading && Boolean(searchText) && !searchResults?.length && ( - <Flex align="center" justify="center" style={{flex: 1}}> - <Typography.Text type="secondary"> - {trans("iconScout.noResults")} - </Typography.Text> - </Flex> - )} - {!loading && Boolean(searchText) && searchResults?.length && ( - <IconListWrapper> - <IconList - width={550} - height={400} - rowHeight={80} - rowCount={Math.ceil(searchResults.length / columnNum)} - rowRenderer={rowRenderer} - /> - </IconListWrapper> - )} + <Spin spinning={downloading} indicator={<LoadingOutlined style={{ fontSize: 25 }} />} > + {!loading && Boolean(searchText) && !searchResults?.length && ( + <Flex align="center" justify="center" style={{flex: 1}}> + <Typography.Text type="secondary"> + {trans("iconScout.noResults")} + </Typography.Text> + </Flex> + )} + {!loading && Boolean(searchText) && searchResults?.length && ( + <IconListWrapper> + <IconList + width={550} + height={400} + rowHeight={80} + rowCount={Math.ceil(searchResults.length / columnNum)} + rowRenderer={rowRenderer} + /> + </IconListWrapper> + )} + </Spin> </PopupContainer> </Draggable> } From aff1582c243a1723063041dbe6860cf94445106f Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> Date: Tue, 25 Feb 2025 22:30:45 +0500 Subject: [PATCH 12/18] added autoHeight and aspectRatio options in json lottie --- .../comps/comps/jsonComp/jsonLottieComp.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index 08d0b08fd3..b982fd5873 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"; @@ -20,6 +21,7 @@ import { defaultLottie } from "./jsonConstants"; import { EditorContext } from "comps/editorState"; import { AssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; import { DotLottie } from "@lottiefiles/dotlottie-react"; +import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl"; // const Player = lazy( // () => import('@lottiefiles/react-lottie-player') @@ -112,6 +114,8 @@ let JsonLottieTmpComp = (function () { animationStart: dropdownControl(animationStartOptions, "auto"), loop: dropdownControl(loopOptions, "single"), keepLastFrame: BoolControl.DEFAULT_TRUE, + autoHeight: withDefault(AutoHeightControl, "fixed"), + aspectRatio: withDefault(StringControl, "16 / 9"), }; return new UICompBuilder(childrenMap, (props, dispatch) => { const [dotLottie, setDotLottie] = useState<DotLottie | null>(null); @@ -151,6 +155,7 @@ let JsonLottieTmpComp = (function () { background: `${props.container.background}`, padding: `${props.container.padding}`, rotate: props.container.rotation, + aspectRatio: props.aspectRatio, }} > <DotLottiePlayer @@ -171,6 +176,9 @@ let JsonLottieTmpComp = (function () { }} onMouseEnter={() => props.animationStart === "hover" && dotLottie?.play()} onMouseLeave={() => props.animationStart === "hover" && dotLottie?.pause()} + renderConfig={{ + autoResize: props.autoHeight, + }} /> </div> </div> @@ -204,6 +212,15 @@ let JsonLottieTmpComp = (function () { </> )} + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( + <Section name={sectionNames.layout}> + {children.autoHeight.getPropertyView()} + {children.aspectRatio.propertyView({ + label: trans("style.aspectRatio"), + })} + </Section> + )} + {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && ( <> <Section name={sectionNames.style}> @@ -221,7 +238,7 @@ let JsonLottieTmpComp = (function () { })(); JsonLottieTmpComp = class extends JsonLottieTmpComp { override autoHeight(): boolean { - return false; + return this.children.autoHeight.getView(); } }; export const JsonLottieComp = withExposingConfigs(JsonLottieTmpComp, [ From 0dc69c6d4d710669892ecfb98cc181f19c54a980 Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> Date: Wed, 26 Feb 2025 12:57:10 +0500 Subject: [PATCH 13/18] fixed selected icon preview in controlButton --- .../lowcoder/src/comps/comps/meetingComp/controlButton.tsx | 3 +-- .../packages/lowcoder/src/comps/controls/iconscoutControl.tsx | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx index e1d1158de0..0e31deb507 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx @@ -77,14 +77,13 @@ const IconWrapper = styled.div<{ $style: any }>` const IconScoutWrapper = styled.div<{ $style: any }>` display: flex; - height: 100%; ${(props) => props.$style && getStyleIcon(props.$style)} `; function getStyleIcon(style: any) { return css` - svg { + svg, img { width: ${style.size} !important; height: ${style.size} !important; } diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index 003e3453b3..5814c22e2f 100644 --- a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -51,6 +51,7 @@ const StyledDeleteInputIcon = styled(DeleteInputIcon)` const StyledImage = styled.img` height: 100%; + width: 100%; color: currentColor; `; From 6886a0ee354cac603d848f212775c7d9addc19be Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> Date: Wed, 26 Feb 2025 22:53:49 +0500 Subject: [PATCH 14/18] added fit/align controls in json lottie comp --- .../comps/comps/jsonComp/jsonLottieComp.tsx | 64 +++++++++++++++++-- .../src/comps/controls/iconscoutControl.tsx | 4 +- .../packages/lowcoder/src/i18n/locales/en.ts | 4 +- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index b982fd5873..7ecec0426c 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx @@ -22,6 +22,7 @@ import { EditorContext } from "comps/editorState"; import { AssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; import { DotLottie } from "@lottiefiles/dotlottie-react"; import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl"; +import { useResizeDetector } from "react-resize-detector"; // const Player = lazy( // () => import('@lottiefiles/react-lottie-player') @@ -93,6 +94,27 @@ const speedOptions = [ }, ] as const; +const alignOptions = [ + { label: "None", value: "none" }, + { label: "Fill", value: "fill" }, + { label: "Cover", value: "cover" }, + { label: "Contain", value: "contain" }, + { label: "Fit Width", value: "fit-width" }, + { label: "Fit Height", value: "fit-height" }, +] as const; + +const fitOptions = [ + { label: "Top Left", value: "0,0" }, + { label: "Top Center", value: "0.5,0" }, + { label: "Top Right", value: "1,0" }, + { label: "Center Left", value: "0,0.5" }, + { label: "Center", value: "0.5,0.5" }, + { label: "Center Right", value: "1,0.5" }, + { label: "Bottom Left", value: "0,1" }, + { label: "Bottom Center", value: "0.5,1" }, + { label: "Bottom Right", value: "1,1" }, +] as const; + const ModeOptions = [ { label: "Lottie JSON", value: "standard" }, { label: "Asset Library", value: "asset-library" } @@ -114,30 +136,59 @@ let JsonLottieTmpComp = (function () { animationStart: dropdownControl(animationStartOptions, "auto"), loop: dropdownControl(loopOptions, "single"), keepLastFrame: BoolControl.DEFAULT_TRUE, - autoHeight: withDefault(AutoHeightControl, "fixed"), - aspectRatio: withDefault(StringControl, "16 / 9"), + autoHeight: withDefault(AutoHeightControl, "auto"), + aspectRatio: withDefault(StringControl, "1/1"), + fit: dropdownControl(alignOptions, "contain"), + align: dropdownControl(fitOptions, "0.5,0.5"), }; return new UICompBuilder(childrenMap, (props, dispatch) => { const [dotLottie, setDotLottie] = useState<DotLottie | null>(null); - + + const setLayoutAndResize = () => { + const align = props.align.split(','); + dotLottie?.setLayout({fit: props.fit, align: [Number(align[0]), Number(align[1])]}) + dotLottie?.resize(); + } + + const { ref: wrapperRef } = useResizeDetector({ + onResize: () => { + if (dotLottie) { + setLayoutAndResize(); + } + } + }); + useEffect(() => { const onComplete = () => { props.keepLastFrame && dotLottie?.setFrame(100); } + const onLoad = () => { + setLayoutAndResize(); + } + if (dotLottie) { dotLottie.addEventListener('complete', onComplete); + dotLottie.addEventListener('load', onLoad); } return () => { if (dotLottie) { dotLottie.removeEventListener('complete', onComplete); + dotLottie.removeEventListener('load', onLoad); } }; }, [dotLottie, props.keepLastFrame]); + useEffect(() => { + if (dotLottie) { + setLayoutAndResize(); + } + }, [dotLottie, props.fit, props.align, props.autoHeight]); + return ( <div + ref={wrapperRef} style={{ height: '100%', padding: `${props.container.margin}`, @@ -155,7 +206,6 @@ let JsonLottieTmpComp = (function () { background: `${props.container.background}`, padding: `${props.container.padding}`, rotate: props.container.rotation, - aspectRatio: props.aspectRatio, }} > <DotLottiePlayer @@ -173,12 +223,10 @@ let JsonLottieTmpComp = (function () { width: "100%", maxWidth: "100%", maxHeight: "100%", + aspectRatio: props.aspectRatio, }} onMouseEnter={() => props.animationStart === "hover" && dotLottie?.play()} onMouseLeave={() => props.animationStart === "hover" && dotLottie?.pause()} - renderConfig={{ - autoResize: props.autoHeight, - }} /> </div> </div> @@ -218,6 +266,8 @@ let JsonLottieTmpComp = (function () { {children.aspectRatio.propertyView({ label: trans("style.aspectRatio"), })} + {children.align.propertyView({ label: trans("jsonLottie.align")})} + {children.fit.propertyView({ label: trans("jsonLottie.fit")})} </Section> )} diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index 5814c22e2f..e3a590450e 100644 --- a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -390,14 +390,14 @@ export const IconPicker = (props: { </Flex> )} <Spin spinning={downloading} indicator={<LoadingOutlined style={{ fontSize: 25 }} />} > - {!loading && Boolean(searchText) && !searchResults?.length && ( + {!loading && Boolean(searchText) && !Boolean(searchResults?.length) && ( <Flex align="center" justify="center" style={{flex: 1}}> <Typography.Text type="secondary"> {trans("iconScout.noResults")} </Typography.Text> </Flex> )} - {!loading && Boolean(searchText) && searchResults?.length && ( + {!loading && Boolean(searchText) && Boolean(searchResults?.length) && ( <IconListWrapper> <IconList width={550} diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index cfc80833f5..8946f0bf66 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3783,7 +3783,9 @@ export const en = { "onHover": "On Hover", "singlePlay": "Single Play", "endlessLoop": "Endless Loop", - "keepLastFrame": "Keep Last Frame displayed" + "keepLastFrame": "Keep Last Frame displayed", + "fit": "Fit", + "align": "Align", }, "timeLine": { "titleColor": "Title Color", From a29d73537f8b765a5577001cc92120931e5131df Mon Sep 17 00:00:00 2001 From: FalkWolsky <fw@falkwolsky.com> Date: Wed, 26 Feb 2025 21:43:09 +0100 Subject: [PATCH 15/18] Changing to Flow API --- .../packages/lowcoder/src/api/iconFlowApi.ts | 163 ++++++++++++++++++ .../packages/lowcoder/src/api/iconscoutApi.ts | 75 +------- .../lowcoder/src/api/subscriptionApi.ts | 5 - .../src/comps/controls/iconscoutControl.tsx | 28 +-- 4 files changed, 184 insertions(+), 87 deletions(-) create mode 100644 client/packages/lowcoder/src/api/iconFlowApi.ts diff --git a/client/packages/lowcoder/src/api/iconFlowApi.ts b/client/packages/lowcoder/src/api/iconFlowApi.ts new file mode 100644 index 0000000000..5bf664c220 --- /dev/null +++ b/client/packages/lowcoder/src/api/iconFlowApi.ts @@ -0,0 +1,163 @@ +import Api from "api/api"; +import axios, { AxiosInstance, AxiosPromise, AxiosRequestConfig } from "axios"; +import { calculateFlowCode } from "./apiUtils"; + +export interface SearchParams { + query: string; + asset: string; + per_page: number; + page: 1; + sort: string; + formats?: string; + price?: string; +} + +export type ResponseType = { + response: any; +}; + +const lcHeaders = { + "Lowcoder-Token": calculateFlowCode(), + "Content-Type": "application/json" +}; + +let axiosIns: AxiosInstance | null = null; + +const getAxiosInstance = (clientSecret?: string) => { + if (axiosIns && !clientSecret) { + return axiosIns; + } + + const headers: Record<string, string> = { + "Content-Type": "application/json", + }; + + const apiRequestConfig: AxiosRequestConfig = { + baseURL: "https://api-service.lowcoder.cloud/api/flow", + headers, + }; + + axiosIns = axios.create(apiRequestConfig); + return axiosIns; +} + +class IconFlowApi extends Api { + + static async secureRequest(body: any, timeout: number = 6000): Promise<any> { + let response; + const axiosInstance = getAxiosInstance(); + + // Create a cancel token and set timeout for cancellation + const source = axios.CancelToken.source(); + const timeoutId = setTimeout(() => { + source.cancel("Request timed out."); + }, timeout); + + // Request configuration with cancel token + const requestConfig: AxiosRequestConfig = { + method: "POST", + withCredentials: true, + data: body, + cancelToken: source.token, // Add cancel token + }; + + try { + response = await axiosInstance.request(requestConfig); + } catch (error) { + if (axios.isCancel(error)) { + // Retry once after timeout cancellation + try { + // Reset the cancel token and retry + const retrySource = axios.CancelToken.source(); + const retryTimeoutId = setTimeout(() => { + retrySource.cancel("Retry request timed out."); + }, 10000); + + response = await axiosInstance.request({ + ...requestConfig, + cancelToken: retrySource.token, + }); + + clearTimeout(retryTimeoutId); + } catch (retryError) { + console.warn("Error at Secure Flow Request. Retry failed:", retryError); + throw retryError; + } + } else { + console.warn("Error at Secure Flow Request:", error); + throw error; + } + } finally { + clearTimeout(timeoutId); // Clear the initial timeout + } + + return response; + } + +} + +export const searchAssets = async (searchParameters : SearchParams) => { + const apiBody = { + path: "webhook/scout/search-asset", + data: searchParameters, + method: "post", + headers: lcHeaders + }; + try { + const result = await IconFlowApi.secureRequest(apiBody); + return result?.response?.items?.total > 0 ? result.response.items as any : null; + } catch (error) { + console.error("Error searching Design Assets:", error); + throw error; + } +}; + +export const getAssetLinks = async (uuid: string, params: Record<string, string>) => { + const apiBody = { + path: "webhook/scout/get-asset-links", + data: params, + method: "post", + headers: lcHeaders + }; + try { + const result = await IconFlowApi.secureRequest(apiBody); + + return result?.response?.items?.total > 0 ? result.response.items as any : null; + } catch (error) { + console.error("Error searching Design Assets:", error); + throw error; + } +}; + + +/* + +static async search(params: SearchParams): Promise<any> { + let response; + try { + response = await getAxiosInstance().request({ + url: '/v3/search', + method: "GET", + withCredentials: false, + params: { + ...params, + }, + }); + } catch (error) { + console.error(error); + } + return response?.data.response.items; + } + + static async download(uuid: string, params: Record<string, string>): Promise<any> { + const response = await getAxiosInstance(clientSecret).request({ + url: `/v3/items/${uuid}/api-download?format=${params.format}`, + method: "POST", + withCredentials: false, + }); + return response?.data.response.download; + } + +*/ + +export default IconFlowApi; \ No newline at end of file diff --git a/client/packages/lowcoder/src/api/iconscoutApi.ts b/client/packages/lowcoder/src/api/iconscoutApi.ts index 71ea45adee..0ad5bf2569 100644 --- a/client/packages/lowcoder/src/api/iconscoutApi.ts +++ b/client/packages/lowcoder/src/api/iconscoutApi.ts @@ -1,84 +1,15 @@ 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; - price?: string; -} +import axios from "axios"; export type ResponseType = { response: any; }; -const apiUrl = "https://api.iconscout.com"; -const clientID = ""; -const clientSecret = ""; -const currentPage = 1; -const currentQuery = ''; -const currentData = []; - -let axiosIns: AxiosInstance | null = null; - -const getAxiosInstance = (clientSecret?: string) => { - if (axiosIns && !clientSecret) { - return axiosIns; - } - - const headers: Record<string, string> = { - "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<any> { - let response; - try { - response = await getAxiosInstance().request({ - url: '/v3/search', - method: "GET", - withCredentials: false, - params: { - ...params, - }, - }); - } catch (error) { - console.error(error); - } - return response?.data.response.items; - } - - static async download(uuid: string, params: Record<string, string>): Promise<any> { - const response = await getAxiosInstance(clientSecret).request({ - url: `/v3/items/${uuid}/api-download?format=${params.format}`, - method: "POST", - withCredentials: false, - }); - return response?.data.response.download; - } - +class IconScoutApi extends Api { static async downloadAsset(url: string): Promise<any> { const response = await axios.get(url, {responseType: 'blob'}) return response?.data; } } -export default IconscoutApi; +export default IconScoutApi; \ No newline at end of file diff --git a/client/packages/lowcoder/src/api/subscriptionApi.ts b/client/packages/lowcoder/src/api/subscriptionApi.ts index 6bfcdb2599..7e19c8f19d 100644 --- a/client/packages/lowcoder/src/api/subscriptionApi.ts +++ b/client/packages/lowcoder/src/api/subscriptionApi.ts @@ -1,11 +1,6 @@ import Api from "api/api"; import axios, { AxiosInstance, AxiosRequestConfig, CancelToken } from "axios"; -import { useDispatch, useSelector } from "react-redux"; -import { useEffect, useState} from "react"; import { calculateFlowCode } from "./apiUtils"; -import { fetchGroupsAction, fetchOrgUsersAction } from "redux/reduxActions/orgActions"; -import { getOrgUsers } from "redux/selectors/orgSelectors"; -import { AppState } from "@lowcoder-ee/redux/reducers"; import type { LowcoderNewCustomer, LowcoderSearchCustomer, diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index e3a590450e..08ca10c53d 100644 --- a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -17,7 +17,8 @@ import styled from "styled-components"; import Popover from "antd/es/popover"; import { CloseIcon, SearchIcon } from "icons"; import Draggable from "react-draggable"; -import IconscoutApi, { SearchParams } from "api/iconscoutApi"; +import IconScoutApi from "@lowcoder-ee/api/iconScoutApi"; +import { searchAssets, getAssetLinks, SearchParams } from "@lowcoder-ee/api/iconFlowApi"; import List, { ListRowProps } from "react-virtualized/dist/es/List"; import { debounce } from "lodash"; import Spin from "antd/es/spin"; @@ -204,7 +205,6 @@ export type IconScoutAsset = { const IconScoutSearchParams: SearchParams = { query: '', - product_type: 'item', asset: 'icon', per_page: 25, page: 1, @@ -240,13 +240,13 @@ export const IconPicker = (props: { const fetchResults = async (query: string) => { setLoading(true); - const freeResult = await IconscoutApi.search({ + const freeResult = await searchAssets({ ...IconScoutSearchParams, asset: props.assetType, price: 'free', query, }); - const premiumResult = await IconscoutApi.search({ + const premiumResult = await searchAssets({ ...IconScoutSearchParams, asset: props.assetType, price: 'premium', @@ -263,7 +263,7 @@ export const IconPicker = (props: { ) => { try { if (uuid && downloadUrl) { - const json = await IconscoutApi.downloadAsset(downloadUrl); + const json = await IconScoutApi.downloadAsset(downloadUrl); getBase64(json, (url: string) => { callback(url); }); @@ -277,7 +277,7 @@ export const IconPicker = (props: { const fetchDownloadUrl = async (uuid: string, preview: string) => { try { setDownloading(true); - const result = await IconscoutApi.download(uuid, { + const result = await getAssetLinks(uuid, { format: props.assetType === AssetType.LOTTIE ? 'lottie' : 'svg', }); @@ -291,10 +291,18 @@ export const IconPicker = (props: { } } - const handleChange = debounce((e) => { - fetchResults(e.target.value); - setSearchText(e.target.value); - }, 500); + const handleChange = (e: { target: { value: any; }; }) => { + const query = e.target.value; + setSearchText(query); // Update search text immediately + + if (query.length > 2) { + debouncedFetchResults(query); // Trigger search only for >2 characters + } else { + setSearchResults([]); // Clear results if input is too short + } + }; + + const debouncedFetchResults = useMemo(() => debounce(fetchResults, 700), []); const rowRenderer = useCallback( (p: ListRowProps) => ( From 516d9d216e67b0721244eb053f1df853e6ddf602 Mon Sep 17 00:00:00 2001 From: FalkWolsky <fw@falkwolsky.com> Date: Wed, 26 Feb 2025 23:04:39 +0100 Subject: [PATCH 16/18] Search and AssetURL APis changed to Flow Endpoint --- client/packages/lowcoder/src/api/iconFlowApi.ts | 8 ++++---- .../lowcoder/src/comps/controls/iconscoutControl.tsx | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/client/packages/lowcoder/src/api/iconFlowApi.ts b/client/packages/lowcoder/src/api/iconFlowApi.ts index 5bf664c220..ba6e6bea0f 100644 --- a/client/packages/lowcoder/src/api/iconFlowApi.ts +++ b/client/packages/lowcoder/src/api/iconFlowApi.ts @@ -71,7 +71,7 @@ class IconFlowApi extends Api { const retrySource = axios.CancelToken.source(); const retryTimeoutId = setTimeout(() => { retrySource.cancel("Retry request timed out."); - }, 10000); + }, 20000); response = await axiosInstance.request({ ...requestConfig, @@ -105,7 +105,7 @@ export const searchAssets = async (searchParameters : SearchParams) => { }; try { const result = await IconFlowApi.secureRequest(apiBody); - return result?.response?.items?.total > 0 ? result.response.items as any : null; + return result?.data?.response?.items?.total > 0 ? result.data.response.items as any : null; } catch (error) { console.error("Error searching Design Assets:", error); throw error; @@ -115,14 +115,14 @@ export const searchAssets = async (searchParameters : SearchParams) => { export const getAssetLinks = async (uuid: string, params: Record<string, string>) => { const apiBody = { path: "webhook/scout/get-asset-links", - data: params, + data: {"uuid" : uuid, "params" : params}, method: "post", headers: lcHeaders }; try { const result = await IconFlowApi.secureRequest(apiBody); - return result?.response?.items?.total > 0 ? result.response.items as any : null; + return result?.data?.response?.download?.url.length > 0 ? result.data.response.download as any : null; } catch (error) { console.error("Error searching Design Assets:", error); throw error; diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index 08ca10c53d..cefb85b3a1 100644 --- a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -253,6 +253,9 @@ export const IconPicker = (props: { query, }); setLoading(false); + + console.log("freeResult", freeResult, "premiumResult", premiumResult) + setSearchResults([...freeResult.data, ...premiumResult.data]); }; From 4b50843077979d9a0a11852c702454b210a7b13f Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> Date: Thu, 27 Feb 2025 22:39:50 +0500 Subject: [PATCH 17/18] added events + exposed play/pause/stop methods with json lottie comp --- .../comps/comps/jsonComp/jsonLottieComp.tsx | 87 +++++++++++++++++-- .../src/comps/controls/iconscoutControl.tsx | 2 +- .../packages/lowcoder/src/i18n/locales/en.ts | 6 ++ 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index 7ecec0426c..4cb3881beb 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx @@ -11,7 +11,7 @@ import { AnimationStyle, LottieStyle } from "comps/controls/styleControlConstant import { trans } from "i18n"; import { Section, sectionNames } from "lowcoder-design"; import { useContext, lazy, useEffect, useState } from "react"; -import { UICompBuilder, withDefault } from "../../generators"; +import { stateComp, UICompBuilder, withDefault } from "../../generators"; import { NameConfig, NameConfigHidden, @@ -23,6 +23,9 @@ import { AssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconsco import { DotLottie } from "@lottiefiles/dotlottie-react"; import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl"; import { useResizeDetector } from "react-resize-detector"; +import { eventHandlerControl } from "@lowcoder-ee/comps/controls/eventHandlerControl"; +import { withMethodExposing } from "@lowcoder-ee/comps/generators/withMethodExposing"; +import { changeChildAction } from "lowcoder-core"; // const Player = lazy( // () => import('@lottiefiles/react-lottie-player') @@ -46,6 +49,10 @@ const animationStartOptions = [ label: trans("jsonLottie.onHover"), value: "hover", }, + { + label: trans("jsonLottie.onTrigger"), + value: "trigger", + }, ] as const; const loopOptions = [ @@ -120,6 +127,14 @@ const ModeOptions = [ { label: "Asset Library", value: "asset-library" } ] as const; +const EventOptions = [ + { label: trans("jsonLottie.load"), value: "load", description: trans("jsonLottie.load") }, + { label: trans("jsonLottie.play"), value: "play", description: trans("jsonLottie.play") }, + { label: trans("jsonLottie.pause"), value: "pause", description: trans("jsonLottie.pause") }, + { label: trans("jsonLottie.stop"), value: "stop", description: trans("jsonLottie.stop") }, + { label: trans("jsonLottie.complete"), value: "complete", description: trans("jsonLottie.complete") }, +] as const;; + let JsonLottieTmpComp = (function () { const childrenMap = { sourceMode: dropdownControl(ModeOptions, "standard"), @@ -140,6 +155,8 @@ let JsonLottieTmpComp = (function () { aspectRatio: withDefault(StringControl, "1/1"), fit: dropdownControl(alignOptions, "contain"), align: dropdownControl(fitOptions, "0.5,0.5"), + onEvent: eventHandlerControl(EventOptions), + dotLottieRef: stateComp<any | null>(null), }; return new UICompBuilder(childrenMap, (props, dispatch) => { const [dotLottie, setDotLottie] = useState<DotLottie | null>(null); @@ -161,21 +178,41 @@ let JsonLottieTmpComp = (function () { useEffect(() => { const onComplete = () => { props.keepLastFrame && dotLottie?.setFrame(100); + props.onEvent('complete'); } const onLoad = () => { setLayoutAndResize(); + props.onEvent('load'); + } + + const onPlay = () => { + props.onEvent('play'); + } + + const onPause = () => { + props.onEvent('pause'); + } + + const onStop = () => { + props.onEvent('stop'); } if (dotLottie) { dotLottie.addEventListener('complete', onComplete); dotLottie.addEventListener('load', onLoad); + dotLottie.addEventListener('play', onPlay); + dotLottie.addEventListener('pause', onPause); + dotLottie.addEventListener('stop', onStop); } return () => { if (dotLottie) { dotLottie.removeEventListener('complete', onComplete); dotLottie.removeEventListener('load', onLoad); + dotLottie.removeEventListener('play', onPlay); + dotLottie.removeEventListener('pause', onPause); + dotLottie.removeEventListener('stop', onStop); } }; }, [dotLottie, props.keepLastFrame]); @@ -212,17 +249,18 @@ let JsonLottieTmpComp = (function () { key={ [props.speed, props.animationStart, props.loop, props.value, props.keepLastFrame] as any } - dotLottieRefCallback={setDotLottie} + dotLottieRefCallback={(lottieRef) => { + setDotLottie(lottieRef); + dispatch( + changeChildAction("dotLottieRef", lottieRef as any, false) + ) + }} autoplay={props.animationStart === "auto"} loop={props.loop === "single" ? false : true} speed={Number(props.speed)} data={props.sourceMode === 'standard' ? props.value as Record<string, undefined> : undefined} src={props.sourceMode === 'asset-library' ? props.iconScoutAsset?.value : undefined} style={{ - height: "100%", - width: "100%", - maxWidth: "100%", - maxHeight: "100%", aspectRatio: props.aspectRatio, }} onMouseEnter={() => props.animationStart === "hover" && dotLottie?.play()} @@ -250,11 +288,12 @@ let JsonLottieTmpComp = (function () { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <><Section name={sectionNames.interaction}> + {children.onEvent.getPropertyView()} {children.speed.propertyView({ label: trans("jsonLottie.speed")})} {children.loop.propertyView({ label: trans("jsonLottie.loop")})} {children.animationStart.propertyView({ label: trans("jsonLottie.animationStart")})} - {children.keepLastFrame.propertyView({ label: trans("jsonLottie.keepLastFrame")})} {hiddenPropertyView(children)} + {children.keepLastFrame.propertyView({ label: trans("jsonLottie.keepLastFrame")})} {showDataLoadingIndicatorsPropertyView(children)} </Section> </> @@ -291,6 +330,40 @@ JsonLottieTmpComp = class extends JsonLottieTmpComp { return this.children.autoHeight.getView(); } }; + +JsonLottieTmpComp = withMethodExposing(JsonLottieTmpComp, [ + { + method: { + name: "play", + description: trans("jsonLottie.play"), + params: [], + }, + execute: (comp) => { + (comp.children.dotLottieRef.value as unknown as DotLottie)?.play(); + }, + }, + { + method: { + name: "pause", + description: trans("jsonLottie.pause"), + params: [], + }, + execute: (comp) => { + (comp.children.dotLottieRef.value as unknown as DotLottie)?.pause(); + }, + }, + { + method: { + name: "stop", + description: trans("jsonLottie.stop"), + params: [], + }, + execute: (comp) => { + (comp.children.dotLottieRef.value as unknown as DotLottie)?.stop(); + }, + }, +]); + export const JsonLottieComp = withExposingConfigs(JsonLottieTmpComp, [ new NameConfig("value", trans("jsonLottie.valueDesc")), NameConfigHidden, diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index cefb85b3a1..3370788ac9 100644 --- a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -17,7 +17,7 @@ import styled from "styled-components"; import Popover from "antd/es/popover"; import { CloseIcon, SearchIcon } from "icons"; import Draggable from "react-draggable"; -import IconScoutApi from "@lowcoder-ee/api/iconScoutApi"; +import IconScoutApi from "@lowcoder-ee/api/iconscoutApi"; import { searchAssets, getAssetLinks, SearchParams } from "@lowcoder-ee/api/iconFlowApi"; import List, { ListRowProps } from "react-virtualized/dist/es/List"; import { debounce } from "lodash"; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 8946f0bf66..3c82c2212b 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3781,11 +3781,17 @@ export const en = { "loop": "Loop", "auto": "Auto", "onHover": "On Hover", + "onTrigger": "On Trigger", "singlePlay": "Single Play", "endlessLoop": "Endless Loop", "keepLastFrame": "Keep Last Frame displayed", "fit": "Fit", "align": "Align", + "load": "On Load", + "play": "On Play", + "pause": "On Pause", + "stop": "On Stop", + "complete": "On Complete", }, "timeLine": { "titleColor": "Title Color", From 99f572515e29285e72e4a6096a1f647fe8ca2a1c Mon Sep 17 00:00:00 2001 From: RAHEEL <mraheeliftikhar1994@gmail.com> Date: Sat, 19 Apr 2025 01:52:59 +0500 Subject: [PATCH 18/18] allow media pack subscribers to use icon scout assets --- client/packages/lowcoder/src/app.tsx | 57 ++-- .../src/comps/controls/iconscoutControl.tsx | 9 +- .../src/pages/ApplicationV2/index.tsx | 315 +++++++++--------- 3 files changed, 194 insertions(+), 187 deletions(-) diff --git a/client/packages/lowcoder/src/app.tsx b/client/packages/lowcoder/src/app.tsx index 05dbeaab25..1ace7b0225 100644 --- a/client/packages/lowcoder/src/app.tsx +++ b/client/packages/lowcoder/src/app.tsx @@ -60,6 +60,7 @@ import GlobalInstances from 'components/GlobalInstances'; import { fetchHomeData, fetchServerSettingsAction } from "./redux/reduxActions/applicationActions"; import { getNpmPackageMeta } from "./comps/utils/remote"; import { packageMetaReadyAction, setLowcoderCompsLoading } from "./redux/reduxActions/npmPluginActions"; +import { SimpleSubscriptionContextProvider } from "./util/context/SimpleSubscriptionContext"; const LazyUserAuthComp = React.lazy(() => import("pages/userAuth")); const LazyInviteLanding = React.lazy(() => import("pages/common/inviteLanding")); @@ -310,33 +311,35 @@ class AppIndex extends React.Component<AppIndexProps, any> { component={LazyPublicAppEditor} /> - <LazyRoute - fallback="layout" - path={APP_EDITOR_URL} - component={LazyAppEditor} - /> - <LazyRoute - fallback="layout" - path={[ - USER_PROFILE_URL, - NEWS_URL, - ORG_HOME_URL, - ALL_APPLICATIONS_URL, - DATASOURCE_CREATE_URL, - DATASOURCE_EDIT_URL, - DATASOURCE_URL, - SUPPORT_URL, - QUERY_LIBRARY_URL, - FOLDERS_URL, - FOLDER_URL, - TRASH_URL, - SETTING_URL, - MARKETPLACE_URL, - ADMIN_APP_URL - ]} - // component={ApplicationListPage} - component={LazyApplicationHome} - /> + <SimpleSubscriptionContextProvider> + <LazyRoute + fallback="layout" + path={APP_EDITOR_URL} + component={LazyAppEditor} + /> + <LazyRoute + fallback="layout" + path={[ + USER_PROFILE_URL, + NEWS_URL, + ORG_HOME_URL, + ALL_APPLICATIONS_URL, + DATASOURCE_CREATE_URL, + DATASOURCE_EDIT_URL, + DATASOURCE_URL, + SUPPORT_URL, + QUERY_LIBRARY_URL, + FOLDERS_URL, + FOLDER_URL, + TRASH_URL, + SETTING_URL, + MARKETPLACE_URL, + ADMIN_APP_URL + ]} + // component={ApplicationListPage} + component={LazyApplicationHome} + /> + </SimpleSubscriptionContextProvider> <LazyRoute exact path={ADMIN_AUTH_URL} component={LazyUserAuthComp} /> <LazyRoute path={USER_AUTH_URL} component={LazyUserAuthComp} /> <LazyRoute diff --git a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx index 3370788ac9..9f43bda672 100644 --- a/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -30,6 +30,8 @@ import LoadingOutlined from "@ant-design/icons/LoadingOutlined"; import Badge from "antd/es/badge"; import { CrownFilled } from "@ant-design/icons"; import { SUBSCRIPTION_SETTING } from "@lowcoder-ee/constants/routesURL"; +import { useSimpleSubscriptionContext } from "@lowcoder-ee/util/context/SimpleSubscriptionContext"; +import { SubscriptionProductsEnum } from "@lowcoder-ee/constants/subscriptionConstants"; const ButtonWrapper = styled.div` width: 100%; @@ -227,6 +229,11 @@ export const IconPicker = (props: { const [ downloading, setDownloading ] = useState(false) const [ searchText, setSearchText ] = useState<string>('') const [ searchResults, setSearchResults ] = useState<Array<any>>([]); + const { subscriptions } = useSimpleSubscriptionContext(); + + const mediaPackSubscription = subscriptions.find( + sub => sub.product === SubscriptionProductsEnum.MEDIAPACKAGE && sub.status === 'active' + ); const onChangeRef = useRef(props.onChange); onChangeRef.current = props.onChange; @@ -319,7 +326,7 @@ export const IconPicker = (props: { onClick={() => { // check if premium content then show subscription popup // TODO: if user has subscription then skip this if block - if (icon.price !== 0) { + if (!mediaPackSubscription) { CustomModal.confirm({ title: trans("iconScout.buySubscriptionTitle"), content: trans("iconScout.buySubscriptionContent"), diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx index 71c13d039e..5856a131d1 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx @@ -69,7 +69,6 @@ import { SubscriptionProductsEnum } from '@lowcoder-ee/constants/subscriptionCon import AppEditor from "../editor/AppEditor"; import { fetchDeploymentIdAction } from "@lowcoder-ee/redux/reduxActions/configActions"; import { getDeploymentId } from "@lowcoder-ee/redux/selectors/configSelectors"; -import { SimpleSubscriptionContextProvider } from '@lowcoder-ee/util/context/SimpleSubscriptionContext'; import {LoadingBarHideTrigger} from "@lowcoder-ee/util/hideLoading"; const TabLabel = styled.div` @@ -154,171 +153,169 @@ export default function ApplicationHome() { return ( <DivStyled> <LoadingBarHideTrigger /> - <SimpleSubscriptionContextProvider> - <Layout - sections={[ - { - items: [ - { - text: <TabLabel>{trans("home.profile")}</TabLabel>, - routePath: USER_PROFILE_URL, - routeComp: UserProfileView, - icon: ({ selected, ...otherProps }) => selected ? <UserIcon {...otherProps} width={"24px"}/> : <UserIcon {...otherProps} width={"24px"}/>, - mobileVisible: true, - }, - { - text: <TabLabel>{trans("home.news")}</TabLabel>, - routePath: NEWS_URL, - routeComp: NewsView, - icon: ({ selected, ...otherProps }) => selected ? <NewsIcon {...otherProps} width={"24px"}/> : <NewsIcon {...otherProps} width={"24px"}/>, - visible: ({ user }) => user.orgDev, - style: { color: "red" }, - mobileVisible: false, - }, - { - text: <TabLabel>{trans("home.orgHome")}</TabLabel>, - routePath: ORG_HOME_URL, - routePathExact: false, - routeComp: OrgView, - icon: ({ selected, ...otherProps }) => selected ? <WorkspacesIcon {...otherProps} width={"24px"}/> : <WorkspacesIcon {...otherProps} width={"24px"}/>, - visible: ({ user }) => !user.orgDev, - mobileVisible: true, - }, - { - text: <TabLabel>{trans("home.marketplace")}</TabLabel>, - routePath: MARKETPLACE_URL, - routePathExact: false, - routeComp: MarketplaceView, - icon: ({ selected, ...otherProps }) => selected ? <MarketplaceIcon {...otherProps} width={"24px"}/> : <MarketplaceIcon {...otherProps} width={"24px"}/>, - mobileVisible: false, - }, - ] - }, + <Layout + sections={[ + { + items: [ + { + text: <TabLabel>{trans("home.profile")}</TabLabel>, + routePath: USER_PROFILE_URL, + routeComp: UserProfileView, + icon: ({ selected, ...otherProps }) => selected ? <UserIcon {...otherProps} width={"24px"}/> : <UserIcon {...otherProps} width={"24px"}/>, + mobileVisible: true, + }, + { + text: <TabLabel>{trans("home.news")}</TabLabel>, + routePath: NEWS_URL, + routeComp: NewsView, + icon: ({ selected, ...otherProps }) => selected ? <NewsIcon {...otherProps} width={"24px"}/> : <NewsIcon {...otherProps} width={"24px"}/>, + visible: ({ user }) => user.orgDev, + style: { color: "red" }, + mobileVisible: false, + }, + { + text: <TabLabel>{trans("home.orgHome")}</TabLabel>, + routePath: ORG_HOME_URL, + routePathExact: false, + routeComp: OrgView, + icon: ({ selected, ...otherProps }) => selected ? <WorkspacesIcon {...otherProps} width={"24px"}/> : <WorkspacesIcon {...otherProps} width={"24px"}/>, + visible: ({ user }) => !user.orgDev, + mobileVisible: true, + }, + { + text: <TabLabel>{trans("home.marketplace")}</TabLabel>, + routePath: MARKETPLACE_URL, + routePathExact: false, + routeComp: MarketplaceView, + icon: ({ selected, ...otherProps }) => selected ? <MarketplaceIcon {...otherProps} width={"24px"}/> : <MarketplaceIcon {...otherProps} width={"24px"}/>, + mobileVisible: false, + }, + ] + }, - { - items: [ - // { - // text: <MoreFoldersWrapper>{trans("home.allFolders")}</MoreFoldersWrapper>, - // routePath: FOLDERS_URL, - // routeComp: RootFolderListView, - // icon: ({ selected, ...otherProps }) => selected ? <FolderIcon {...otherProps} width={"24px"}/> : <FolderIcon {...otherProps} width={"24px"}/>, - // }, - { - text: <TabLabel>{trans("home.allApplications")}</TabLabel>, - routePath: ALL_APPLICATIONS_URL, - routeComp: HomeView, - icon: ({ selected, ...otherProps }) => selected ? <AppsIcon {...otherProps} width={"24px"}/> : <AppsIcon {...otherProps} width={"24px"}/>, - mobileVisible: true, - }, - ], - }, - - { - items: [ - - { - text: <TabLabel>{trans("home.queryLibrary")}</TabLabel>, - routePath: QUERY_LIBRARY_URL, - routeComp: QueryLibraryEditor, - icon: ({ selected, ...otherProps }) => selected ? <HomeQueryLibraryIcon {...otherProps} width={"24px"}/> : <HomeQueryLibraryIcon {...otherProps} width={"24px"}/>, - visible: ({ user }) => user.orgDev, - mobileVisible: false, - }, - { - text: <TabLabel>{trans("home.datasource")}</TabLabel>, - routePath: DATASOURCE_URL, - routePathExact: false, - routeComp: DatasourceHome, - icon: ({ selected, ...otherProps }) => selected ? <HomeDataSourceIcon {...otherProps} width={"24px"}/> : <HomeDataSourceIcon {...otherProps} width={"24px"}/>, - visible: ({ user }) => user.orgDev, - onSelected: (_, currentPath) => currentPath.split("/")[1] === "datasource", - mobileVisible: false, - }, - ], - }, - isEE() ? { - items: [ - { - text: <TabLabel>{trans("settings.AppUsage")}</TabLabel>, - routePath: "/ee/6600ae8724a23f365ba2ed4c/admin", - routePathExact: false, - routeComp: AppEditor, - icon: ({ selected, ...otherProps }) => selected ? ( <EnterpriseIcon {...otherProps} width={"24px"}/> ) : ( <EnterpriseIcon {...otherProps} width={"24px"}/> ), - visible: ({ user }) => user.orgDev, - mobileVisible: false, - }, - ], - } : { items: [] }, + { + items: [ + // { + // text: <MoreFoldersWrapper>{trans("home.allFolders")}</MoreFoldersWrapper>, + // routePath: FOLDERS_URL, + // routeComp: RootFolderListView, + // icon: ({ selected, ...otherProps }) => selected ? <FolderIcon {...otherProps} width={"24px"}/> : <FolderIcon {...otherProps} width={"24px"}/>, + // }, + { + text: <TabLabel>{trans("home.allApplications")}</TabLabel>, + routePath: ALL_APPLICATIONS_URL, + routeComp: HomeView, + icon: ({ selected, ...otherProps }) => selected ? <AppsIcon {...otherProps} width={"24px"}/> : <AppsIcon {...otherProps} width={"24px"}/>, + mobileVisible: true, + }, + ], + }, + + { + items: [ + + { + text: <TabLabel>{trans("home.queryLibrary")}</TabLabel>, + routePath: QUERY_LIBRARY_URL, + routeComp: QueryLibraryEditor, + icon: ({ selected, ...otherProps }) => selected ? <HomeQueryLibraryIcon {...otherProps} width={"24px"}/> : <HomeQueryLibraryIcon {...otherProps} width={"24px"}/>, + visible: ({ user }) => user.orgDev, + mobileVisible: false, + }, + { + text: <TabLabel>{trans("home.datasource")}</TabLabel>, + routePath: DATASOURCE_URL, + routePathExact: false, + routeComp: DatasourceHome, + icon: ({ selected, ...otherProps }) => selected ? <HomeDataSourceIcon {...otherProps} width={"24px"}/> : <HomeDataSourceIcon {...otherProps} width={"24px"}/>, + visible: ({ user }) => user.orgDev, + onSelected: (_, currentPath) => currentPath.split("/")[1] === "datasource", + mobileVisible: false, + }, + ], + }, + isEE() ? { + items: [ + { + text: <TabLabel>{trans("settings.AppUsage")}</TabLabel>, + routePath: "/ee/6600ae8724a23f365ba2ed4c/admin", + routePathExact: false, + routeComp: AppEditor, + icon: ({ selected, ...otherProps }) => selected ? ( <EnterpriseIcon {...otherProps} width={"24px"}/> ) : ( <EnterpriseIcon {...otherProps} width={"24px"}/> ), + visible: ({ user }) => user.orgDev, + mobileVisible: false, + }, + ], + } : { items: [] }, - !supportSubscription && user.orgDev ? { - items: [ - { - text: <TabLabel>{trans("home.support")}</TabLabel>, - routePath: SUBSCRIPTION_SETTING, - routeComp: Subscription, - routePathExact: false, - icon: ({ selected, ...otherProps }) => selected ? <SupportIcon {...otherProps} width={"24px"}/> : <SupportIcon {...otherProps} width={"24px"}/>, - mobileVisible: true, - }, - ], - } : { items: [] }, + !supportSubscription && user.orgDev ? { + items: [ + { + text: <TabLabel>{trans("home.support")}</TabLabel>, + routePath: SUBSCRIPTION_SETTING, + routeComp: Subscription, + routePathExact: false, + icon: ({ selected, ...otherProps }) => selected ? <SupportIcon {...otherProps} width={"24px"}/> : <SupportIcon {...otherProps} width={"24px"}/>, + mobileVisible: true, + }, + ], + } : { items: [] }, - supportSubscription && user.orgDev ? { - items: [ - { - text: <TabLabel>{trans("home.support")}</TabLabel>, - routePath: SUPPORT_URL, - routeComp: Support, - routePathExact: false, - icon: ({ selected, ...otherProps }) => selected ? <SupportIcon {...otherProps} width={"24px"}/> : <SupportIcon {...otherProps} width={"24px"}/>, - mobileVisible: true, - }, - ], - } : { items: [] }, + supportSubscription && user.orgDev ? { + items: [ + { + text: <TabLabel>{trans("home.support")}</TabLabel>, + routePath: SUPPORT_URL, + routeComp: Support, + routePathExact: false, + icon: ({ selected, ...otherProps }) => selected ? <SupportIcon {...otherProps} width={"24px"}/> : <SupportIcon {...otherProps} width={"24px"}/>, + mobileVisible: true, + }, + ], + } : { items: [] }, - { - items: [ - { - text: <TabLabel>{trans("settings.title")}</TabLabel>, - routePath: SETTING_URL, - routePathExact: false, - routeComp: Setting, - icon: ({ selected, ...otherProps }) => selected ? <HomeSettingIcon {...otherProps} width={"24px"}/> : <HomeSettingIcon {...otherProps} width={"24px"}/>, - visible: ({ user }) => user.orgDev, - onSelected: (_, currentPath) => currentPath.split("/")[1] === "setting", - mobileVisible: false, - } - ] - }, + { + items: [ + { + text: <TabLabel>{trans("settings.title")}</TabLabel>, + routePath: SETTING_URL, + routePathExact: false, + routeComp: Setting, + icon: ({ selected, ...otherProps }) => selected ? <HomeSettingIcon {...otherProps} width={"24px"}/> : <HomeSettingIcon {...otherProps} width={"24px"}/>, + visible: ({ user }) => user.orgDev, + onSelected: (_, currentPath) => currentPath.split("/")[1] === "setting", + mobileVisible: false, + } + ] + }, - { - items: [ - { - text: <TabLabel>{trans("home.trash")}</TabLabel>, - routePath: TRASH_URL, - routeComp: TrashView, - icon: ({ selected, ...otherProps }) => selected ? <RecyclerIcon {...otherProps} width={"24px"}/> : <RecyclerIcon {...otherProps} width={"24px"}/>, - visible: ({ user }) => user.orgDev, - mobileVisible: false, - }, - ], - }, + { + items: [ + { + text: <TabLabel>{trans("home.trash")}</TabLabel>, + routePath: TRASH_URL, + routeComp: TrashView, + icon: ({ selected, ...otherProps }) => selected ? <RecyclerIcon {...otherProps} width={"24px"}/> : <RecyclerIcon {...otherProps} width={"24px"}/>, + visible: ({ user }) => user.orgDev, + mobileVisible: false, + }, + ], + }, - // this we need to show the Folders view in the Admin Area - { - items: [ - { - text: "", - routePath: FOLDER_URL, - routeComp: FolderView, - visible: () => false, - } - ] - } + // this we need to show the Folders view in the Admin Area + { + items: [ + { + text: "", + routePath: FOLDER_URL, + routeComp: FolderView, + visible: () => false, + } + ] + } - ]} - /> - </SimpleSubscriptionContextProvider> + ]} + /> </DivStyled> ); }