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/api/iconFlowApi.ts b/client/packages/lowcoder/src/api/iconFlowApi.ts new file mode 100644 index 0000000000..ba6e6bea0f --- /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 = { + "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 { + 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."); + }, 20000); + + 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?.data?.response?.items?.total > 0 ? result.data.response.items as any : null; + } catch (error) { + console.error("Error searching Design Assets:", error); + throw error; + } +}; + +export const getAssetLinks = async (uuid: string, params: Record) => { + const apiBody = { + path: "webhook/scout/get-asset-links", + data: {"uuid" : uuid, "params" : params}, + method: "post", + headers: lcHeaders + }; + try { + const result = await IconFlowApi.secureRequest(apiBody); + + 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; + } +}; + + +/* + +static async search(params: SearchParams): Promise { + 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): Promise { + 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 new file mode 100644 index 0000000000..0ad5bf2569 --- /dev/null +++ b/client/packages/lowcoder/src/api/iconscoutApi.ts @@ -0,0 +1,15 @@ +import Api from "api/api"; +import axios from "axios"; + +export type ResponseType = { + response: any; +}; + +class IconScoutApi extends Api { + static async downloadAsset(url: string): Promise { + const response = await axios.get(url, {responseType: 'blob'}) + return response?.data; + } +} + +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/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 { component={LazyPublicAppEditor} /> - - + + + + ) => { }} onClick={() => props.onEvent("click")} > - {props.icon} + { props.sourceMode === 'standard' + ? props.icon + : + } )} > @@ -117,11 +129,17 @@ let IconBasicComp = (function () { .setPropertyViewFn((children) => ( <>
- {children.icon.propertyView({ + { children.sourceMode.propertyView({ + label: "", + radioButton: true + })} + {children.sourceMode.getView() === 'standard' && children.icon.propertyView({ label: trans("iconComp.icon"), IconType: "All", })} - + {children.sourceMode.getView() === 'asset-library' && children.iconScoutAsset.propertyView({ + label: trans("button.icon"), + })}
{["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( diff --git a/client/packages/lowcoder/src/comps/comps/imageComp.tsx b/client/packages/lowcoder/src/comps/comps/imageComp.tsx index d78a21d201..1806399e26 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 { AssetType, 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: "Asset Library", value: "asset-library" }, +] as const; const ContainerImg = (props: RecordConstructorToView) => { const imgRef = useRef(null); @@ -194,7 +200,11 @@ const ContainerImg = (props: RecordConstructorToView) => { } > ) => { }; const childrenMap = { + sourceMode: dropdownControl(ModeOptions, "standard"), src: withDefault(StringStateControl, "https://temp.im/350x400"), + iconScoutAsset: IconscoutControl(AssetType.ILLUSTRATION), onEvent: eventHandlerControl(EventOptions), style: styleControl(ImageStyle , 'style'), animationStyle: styleControl(AnimationStyle , 'animationStyle'), @@ -234,7 +246,14 @@ let ImageBasicComp = new UICompBuilder(childrenMap, (props) => { return ( <>
- {children.src.propertyView({ + { children.sourceMode.propertyView({ + label: "", + radioButton: true + })} + {children.sourceMode.getView() === 'standard' && children.src.propertyView({ + label: trans("image.src"), + })} + {children.sourceMode.getView() === 'asset-library' && children.iconScoutAsset.propertyView({ label: trans("image.src"), })}
diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index c3f93b6e1c..4cb3881beb 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"; @@ -9,8 +10,8 @@ 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 { UICompBuilder, withDefault } from "../../generators"; +import { useContext, lazy, useEffect, useState } from "react"; +import { stateComp, UICompBuilder, withDefault } from "../../generators"; import { NameConfig, NameConfigHidden, @@ -18,10 +19,22 @@ import { } from "../../generators/withExposing"; 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"; +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') - .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') + .then(module => ({default: module.DotLottieReact})) ); /** @@ -34,7 +47,11 @@ const animationStartOptions = [ }, { label: trans("jsonLottie.onHover"), - value: "on hover", + value: "hover", + }, + { + label: trans("jsonLottie.onTrigger"), + value: "trigger", }, ] as const; @@ -84,12 +101,48 @@ 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" } +] 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"), value: withDefault( ArrayOrJSONObjectControl, JSON.stringify(defaultLottie, null, 2) ), + iconScoutAsset: IconscoutControl(AssetType.LOTTIE), speed: dropdownControl(speedOptions, "1"), width: withDefault(NumberControl, 100), height: withDefault(NumberControl, 100), @@ -98,11 +151,83 @@ let JsonLottieTmpComp = (function () { animationStart: dropdownControl(animationStartOptions, "auto"), loop: dropdownControl(loopOptions, "single"), keepLastFrame: BoolControl.DEFAULT_TRUE, + autoHeight: withDefault(AutoHeightControl, "auto"), + aspectRatio: withDefault(StringControl, "1/1"), + fit: dropdownControl(alignOptions, "contain"), + align: dropdownControl(fitOptions, "0.5,0.5"), + onEvent: eventHandlerControl(EventOptions), + dotLottieRef: stateComp(null), }; - return new UICompBuilder(childrenMap, (props) => { + return new UICompBuilder(childrenMap, (props, dispatch) => { + const [dotLottie, setDotLottie] = useState(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); + 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]); + + useEffect(() => { + if (dotLottie) { + setLayoutAndResize(); + } + }, [dotLottie, props.fit, props.align, props.autoHeight]); + return (
- { + setDotLottie(lottieRef); + dispatch( + changeChildAction("dotLottieRef", lottieRef as any, false) + ) + }} + autoplay={props.animationStart === "auto"} loop={props.loop === "single" ? false : true} speed={Number(props.speed)} - src={props.value} + data={props.sourceMode === 'standard' ? props.value as Record : 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()} + onMouseLeave={() => props.animationStart === "hover" && dotLottie?.pause()} />
@@ -145,23 +274,42 @@ let JsonLottieTmpComp = (function () { return ( <>
- {children.value.propertyView({ + { children.sourceMode.propertyView({ + label: "", + radioButton: true + })} + {children.sourceMode.getView() === 'standard' && children.value.propertyView({ label: trans("jsonLottie.lottieJson"), })} + {children.sourceMode.getView() === 'asset-library' && children.iconScoutAsset.propertyView({ + label: "Lottie Source", + })}
{(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
+ {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)}
)} + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( +
+ {children.autoHeight.getPropertyView()} + {children.aspectRatio.propertyView({ + label: trans("style.aspectRatio"), + })} + {children.align.propertyView({ label: trans("jsonLottie.align")})} + {children.fit.propertyView({ label: trans("jsonLottie.fit")})} +
+ )} + {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && ( <>
@@ -179,9 +327,43 @@ let JsonLottieTmpComp = (function () { })(); JsonLottieTmpComp = class extends JsonLottieTmpComp { override autoHeight(): boolean { - return false; + 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/comps/meetingComp/controlButton.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/controlButton.tsx index 28e318618c..0e31deb507 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 { AssetType, IconscoutControl } from "@lowcoder-ee/comps/controls/iconscoutControl"; const Container = styled.div<{ $style: any }>` height: 100%; @@ -74,9 +75,15 @@ const IconWrapper = styled.div<{ $style: any }>` ${(props) => props.$style && getStyleIcon(props.$style)} `; +const IconScoutWrapper = styled.div<{ $style: any }>` + display: flex; + + ${(props) => props.$style && getStyleIcon(props.$style)} +`; + function getStyleIcon(style: any) { return css` - svg { + svg, img { width: ${style.size} !important; height: ${style.size} !important; } @@ -163,6 +170,11 @@ const typeOptions = [ }, ] as const; +const ModeOptions = [ + { label: "Standard", value: "standard" }, + { label: "Asset Library", value: "asset-library" }, +] as const; + function isDefault(type?: string) { return !type; } @@ -183,7 +195,9 @@ const childrenMap = { disabled: BoolCodeControl, loading: BoolCodeControl, form: SelectFormControl, + sourceMode: dropdownControl(ModeOptions, "standard"), prefixIcon: IconControl, + iconScoutAsset: IconscoutControl(AssetType.ICON), style: ButtonStyleControl, viewRef: RefControl, restrictPaddingOnRotation:withDefault(StringControl, 'controlButton') @@ -270,14 +284,20 @@ let ButtonTmpComp = (function () { : submitForm(editorState, props.form) } > - {props.prefixIcon && ( + {props.sourceMode === 'standard' && props.prefixIcon && ( {props.prefixIcon} )} - + {props.sourceMode === 'asset-library' && props.iconScoutAsset && ( + + + + )} @@ -291,7 +311,14 @@ let ButtonTmpComp = (function () { .setPropertyViewFn((children) => ( <>
- {children.prefixIcon.propertyView({ + { children.sourceMode.propertyView({ + label: "", + radioButton: true + })} + {children.sourceMode.getView() === 'standard' && children.prefixIcon.propertyView({ + label: trans("button.icon"), + })} + {children.sourceMode.getView() === 'asset-library' &&children.iconScoutAsset.propertyView({ label: trans("button.icon"), })}
@@ -314,13 +341,13 @@ let ButtonTmpComp = (function () { {children.iconSize.propertyView({ label: trans("button.iconSize"), })} -
-
- {children.style.getPropertyView()} {children.aspectRatio.propertyView({ label: trans("style.aspectRatio"), })}
+
+ {children.style.getPropertyView()} +
)} 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..9f43bda672 --- /dev/null +++ b/client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx @@ -0,0 +1,505 @@ +import { trans } from "i18n"; +import { + SimpleComp, +} from "lowcoder-core"; +import { + BlockGrayLabel, + ControlPropertyViewWrapper, + CustomModal, + DeleteInputIcon, + TacoButton, + TacoInput, + useIcon, + wrapperToControlItem, +} from "lowcoder-design"; +import { ReactNode, useCallback, useMemo, useRef, useState } from "react"; +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 { 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"; +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"; +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%; + display: flex; + align-items: center; +`; +const ButtonIconWrapper = styled.div` + display: flex; + width: 18px; +`; + +const StyledDeleteInputIcon = styled(DeleteInputIcon)` + margin-left: auto; + cursor: pointer; + + &:hover circle { + fill: #8b8fa3; + } +`; + +const StyledImage = styled.img` + height: 100%; + width: 100%; + color: currentColor; +`; + +const Wrapper = styled.div` + > div:nth-of-type(1) { + margin-bottom: 4px; + } +`; +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; + 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: 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; + } + + .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; + border-radius: 4px; + background: #fafafa; + + &:hover { + box-shadow: 0 8px 24px #1a29470a,0 2px 8px #1a294714; + } + + &:focus { + border: 1px solid #315efb; + box-shadow: 0 0 0 2px #d6e4ff; + } +`; + +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", + // '3D' = "3d", + LOTTIE = "lottie", +} + +export type IconScoutAsset = { + uuid: string; + value: string; + preview: string; +} + +const IconScoutSearchParams: SearchParams = { + query: '', + asset: 'icon', + per_page: 25, + page: 1, + sort: 'relevant', +}; + +const columnNum = 8; + +export const IconPicker = (props: { + assetType: string; + uuid: string; + value: string; + preview: string; + onChange: (key: string, value: string, preview: string) => void; + label?: ReactNode; + IconType?: "OnlyAntd" | "All" | "default" | undefined; +}) => { + const [ visible, setVisible ] = useState(false) + const [ loading, setLoading ] = useState(false) + const [ downloading, setDownloading ] = useState(false) + const [ searchText, setSearchText ] = useState('') + const [ searchResults, setSearchResults ] = useState>([]); + const { subscriptions } = useSimpleSubscriptionContext(); + + const mediaPackSubscription = subscriptions.find( + sub => sub.product === SubscriptionProductsEnum.MEDIAPACKAGE && sub.status === 'active' + ); + + const onChangeRef = useRef(props.onChange); + onChangeRef.current = props.onChange; + + const onChangeIcon = useCallback( + (key: string, value: string, url: string) => { + onChangeRef.current(key, value, url); + setVisible(false); + }, [] + ); + + const fetchResults = async (query: string) => { + setLoading(true); + const freeResult = await searchAssets({ + ...IconScoutSearchParams, + asset: props.assetType, + price: 'free', + query, + }); + const premiumResult = await searchAssets({ + ...IconScoutSearchParams, + asset: props.assetType, + price: 'premium', + query, + }); + setLoading(false); + + console.log("freeResult", freeResult, "premiumResult", premiumResult) + + setSearchResults([...freeResult.data, ...premiumResult.data]); + }; + + 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); + setDownloading(false); + } + } + + const fetchDownloadUrl = async (uuid: string, preview: string) => { + try { + setDownloading(true); + const result = await getAssetLinks(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); + } + } + + 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) => ( + + {searchResults + .slice(p.index * columnNum, (p.index + 1) * columnNum) + .map((icon) => ( + { + // check if premium content then show subscription popup + // TODO: if user has subscription then skip this if block + if (!mediaPackSubscription) { + 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, + ); + }} + > + : undefined} + size='small' + > + + {props.assetType === AssetType.ICON && ( + + )} + {props.assetType === AssetType.ILLUSTRATION && ( + + )} + {props.assetType === AssetType.LOTTIE && ( + + )} + + + + ))} + + ),[searchResults] + ); + + const popupTitle = useMemo(() => { + 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 ( + + + + {popupTitle} + setVisible(false)} /> + + + + + + {loading && ( + + } /> + + )} + } > + {!loading && Boolean(searchText) && !Boolean(searchResults?.length) && ( + + + {trans("iconScout.noResults")} + + + )} + {!loading && Boolean(searchText) && Boolean(searchResults?.length) && ( + + + + )} + + + + } + > + + {props.preview ? ( + + + {props.assetType === AssetType.LOTTIE && ( + + { + props.onChange("", "", ""); + e.stopPropagation(); + }} + /> + + ) : ( + + )} + + + ); +}; + +export function IconControlView(props: { value: string, uuid: string }) { + const { value } = props; + const icon = useIcon(value); + + if (icon) { + return icon.getView(); + } + return ; +} + +export function IconscoutControl( + assetType: string = AssetType.ICON, +) { + return class IconscoutControl extends SimpleComp { + readonly IGNORABLE_DEFAULT_VALUE = false; + protected getDefaultValue(): IconScoutAsset { + return { + uuid: '', + value: '', + preview: '', + }; + } + + override getPropertyView(): ReactNode { + throw new Error("Method not implemented."); + } + + propertyView(params: ControlParams & { type?: "switch" | "checkbox" }) { + return wrapperToControlItem( + + { + this.dispatchChangeValueAction({uuid, value, preview}) + }} + label={params.label} + IconType={params.IconType} + /> + + ); + } + } +} diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 825761bc3c..db8085af97 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3783,9 +3783,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" + "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", @@ -4090,6 +4098,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); 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 ( - - {trans("home.profile")}, - routePath: USER_PROFILE_URL, - routeComp: UserProfileView, - icon: ({ selected, ...otherProps }) => selected ? : , - mobileVisible: true, - }, - { - text: {trans("home.news")}, - routePath: NEWS_URL, - routeComp: NewsView, - icon: ({ selected, ...otherProps }) => selected ? : , - visible: ({ user }) => user.orgDev, - style: { color: "red" }, - mobileVisible: false, - }, - { - text: {trans("home.orgHome")}, - routePath: ORG_HOME_URL, - routePathExact: false, - routeComp: OrgView, - icon: ({ selected, ...otherProps }) => selected ? : , - visible: ({ user }) => !user.orgDev, - mobileVisible: true, - }, - { - text: {trans("home.marketplace")}, - routePath: MARKETPLACE_URL, - routePathExact: false, - routeComp: MarketplaceView, - icon: ({ selected, ...otherProps }) => selected ? : , - mobileVisible: false, - }, - ] - }, + {trans("home.profile")}, + routePath: USER_PROFILE_URL, + routeComp: UserProfileView, + icon: ({ selected, ...otherProps }) => selected ? : , + mobileVisible: true, + }, + { + text: {trans("home.news")}, + routePath: NEWS_URL, + routeComp: NewsView, + icon: ({ selected, ...otherProps }) => selected ? : , + visible: ({ user }) => user.orgDev, + style: { color: "red" }, + mobileVisible: false, + }, + { + text: {trans("home.orgHome")}, + routePath: ORG_HOME_URL, + routePathExact: false, + routeComp: OrgView, + icon: ({ selected, ...otherProps }) => selected ? : , + visible: ({ user }) => !user.orgDev, + mobileVisible: true, + }, + { + text: {trans("home.marketplace")}, + routePath: MARKETPLACE_URL, + routePathExact: false, + routeComp: MarketplaceView, + icon: ({ selected, ...otherProps }) => selected ? : , + mobileVisible: false, + }, + ] + }, - { - items: [ - // { - // text: {trans("home.allFolders")}, - // routePath: FOLDERS_URL, - // routeComp: RootFolderListView, - // icon: ({ selected, ...otherProps }) => selected ? : , - // }, - { - text: {trans("home.allApplications")}, - routePath: ALL_APPLICATIONS_URL, - routeComp: HomeView, - icon: ({ selected, ...otherProps }) => selected ? : , - mobileVisible: true, - }, - ], - }, - - { - items: [ - - { - text: {trans("home.queryLibrary")}, - routePath: QUERY_LIBRARY_URL, - routeComp: QueryLibraryEditor, - icon: ({ selected, ...otherProps }) => selected ? : , - visible: ({ user }) => user.orgDev, - mobileVisible: false, - }, - { - text: {trans("home.datasource")}, - routePath: DATASOURCE_URL, - routePathExact: false, - routeComp: DatasourceHome, - icon: ({ selected, ...otherProps }) => selected ? : , - visible: ({ user }) => user.orgDev, - onSelected: (_, currentPath) => currentPath.split("/")[1] === "datasource", - mobileVisible: false, - }, - ], - }, - isEE() ? { - items: [ - { - text: {trans("settings.AppUsage")}, - routePath: "/ee/6600ae8724a23f365ba2ed4c/admin", - routePathExact: false, - routeComp: AppEditor, - icon: ({ selected, ...otherProps }) => selected ? ( ) : ( ), - visible: ({ user }) => user.orgDev, - mobileVisible: false, - }, - ], - } : { items: [] }, + { + items: [ + // { + // text: {trans("home.allFolders")}, + // routePath: FOLDERS_URL, + // routeComp: RootFolderListView, + // icon: ({ selected, ...otherProps }) => selected ? : , + // }, + { + text: {trans("home.allApplications")}, + routePath: ALL_APPLICATIONS_URL, + routeComp: HomeView, + icon: ({ selected, ...otherProps }) => selected ? : , + mobileVisible: true, + }, + ], + }, + + { + items: [ + + { + text: {trans("home.queryLibrary")}, + routePath: QUERY_LIBRARY_URL, + routeComp: QueryLibraryEditor, + icon: ({ selected, ...otherProps }) => selected ? : , + visible: ({ user }) => user.orgDev, + mobileVisible: false, + }, + { + text: {trans("home.datasource")}, + routePath: DATASOURCE_URL, + routePathExact: false, + routeComp: DatasourceHome, + icon: ({ selected, ...otherProps }) => selected ? : , + visible: ({ user }) => user.orgDev, + onSelected: (_, currentPath) => currentPath.split("/")[1] === "datasource", + mobileVisible: false, + }, + ], + }, + isEE() ? { + items: [ + { + text: {trans("settings.AppUsage")}, + routePath: "/ee/6600ae8724a23f365ba2ed4c/admin", + routePathExact: false, + routeComp: AppEditor, + icon: ({ selected, ...otherProps }) => selected ? ( ) : ( ), + visible: ({ user }) => user.orgDev, + mobileVisible: false, + }, + ], + } : { items: [] }, - !supportSubscription && user.orgDev ? { - items: [ - { - text: {trans("home.support")}, - routePath: SUBSCRIPTION_SETTING, - routeComp: Subscription, - routePathExact: false, - icon: ({ selected, ...otherProps }) => selected ? : , - mobileVisible: true, - }, - ], - } : { items: [] }, + !supportSubscription && user.orgDev ? { + items: [ + { + text: {trans("home.support")}, + routePath: SUBSCRIPTION_SETTING, + routeComp: Subscription, + routePathExact: false, + icon: ({ selected, ...otherProps }) => selected ? : , + mobileVisible: true, + }, + ], + } : { items: [] }, - supportSubscription && user.orgDev ? { - items: [ - { - text: {trans("home.support")}, - routePath: SUPPORT_URL, - routeComp: Support, - routePathExact: false, - icon: ({ selected, ...otherProps }) => selected ? : , - mobileVisible: true, - }, - ], - } : { items: [] }, + supportSubscription && user.orgDev ? { + items: [ + { + text: {trans("home.support")}, + routePath: SUPPORT_URL, + routeComp: Support, + routePathExact: false, + icon: ({ selected, ...otherProps }) => selected ? : , + mobileVisible: true, + }, + ], + } : { items: [] }, - { - items: [ - { - text: {trans("settings.title")}, - routePath: SETTING_URL, - routePathExact: false, - routeComp: Setting, - icon: ({ selected, ...otherProps }) => selected ? : , - visible: ({ user }) => user.orgDev, - onSelected: (_, currentPath) => currentPath.split("/")[1] === "setting", - mobileVisible: false, - } - ] - }, + { + items: [ + { + text: {trans("settings.title")}, + routePath: SETTING_URL, + routePathExact: false, + routeComp: Setting, + icon: ({ selected, ...otherProps }) => selected ? : , + visible: ({ user }) => user.orgDev, + onSelected: (_, currentPath) => currentPath.split("/")[1] === "setting", + mobileVisible: false, + } + ] + }, - { - items: [ - { - text: {trans("home.trash")}, - routePath: TRASH_URL, - routeComp: TrashView, - icon: ({ selected, ...otherProps }) => selected ? : , - visible: ({ user }) => user.orgDev, - mobileVisible: false, - }, - ], - }, + { + items: [ + { + text: {trans("home.trash")}, + routePath: TRASH_URL, + routeComp: TrashView, + icon: ({ selected, ...otherProps }) => selected ? : , + 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, + } + ] + } - ]} - /> - + ]} + /> ); } 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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy