From 96b90e86081b653d1db4b0e485d4bc51f4f2114e Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Fri, 3 Jan 2025 23:23:14 +0500 Subject: [PATCH 1/3] added public app editor --- client/packages/lowcoder/src/app.tsx | 10 + .../src/comps/comps/remoteComp/loaders.tsx | 1 + .../lowcoder/src/constants/routesURL.ts | 1 + .../lowcoder/src/pages/common/header.tsx | 6 +- .../lowcoder/src/pages/editor/AppEditor.tsx | 1 + .../src/pages/editor/AppEditorPublic.tsx | 237 ++++++++++++++++++ .../src/pages/editor/appEditorInternal.tsx | 1 + .../src/redux/sagas/applicationSagas.ts | 130 +++++++++- .../lowcoder/src/util/pagination/axios.ts | 25 +- 9 files changed, 404 insertions(+), 8 deletions(-) create mode 100644 client/packages/lowcoder/src/pages/editor/AppEditorPublic.tsx diff --git a/client/packages/lowcoder/src/app.tsx b/client/packages/lowcoder/src/app.tsx index 9cd381969..5c7776cba 100644 --- a/client/packages/lowcoder/src/app.tsx +++ b/client/packages/lowcoder/src/app.tsx @@ -29,6 +29,7 @@ import { ORG_AUTH_FORGOT_PASSWORD_URL, ORG_AUTH_RESET_PASSWORD_URL, ADMIN_AUTH_URL, + PUBLIC_APP_EDITOR_URL, } from "constants/routesURL"; import React from "react"; import { createRoot } from "react-dom/client"; @@ -65,6 +66,7 @@ const LazyInviteLanding = React.lazy(() => import("pages/common/inviteLanding")) const LazyComponentDoc = React.lazy(() => import("pages/ComponentDoc")); const LazyComponentPlayground = React.lazy(() => import("pages/ComponentPlayground")); const LazyAppEditor = React.lazy(() => import("pages/editor/AppEditor")); +const LazyPublicAppEditor = React.lazy(() => import("pages/editor/AppEditorPublic")); const LazyAppFromTemplate = React.lazy(() => import("pages/ApplicationV2/AppFromTemplate")); const LazyApplicationHome = React.lazy(() => import("pages/ApplicationV2")); const LazyDebugComp = React.lazy(() => import("./debug")); @@ -301,6 +303,14 @@ class AppIndex extends React.Component { path={IMPORT_APP_FROM_TEMPLATE_URL} component={LazyAppFromTemplate} /> + + + { if(blockEditing && application && Boolean(application?.editingUserId)) { UserApi.getUserDetail(application.editingUserId!) @@ -536,7 +538,7 @@ export default function Header(props: HeaderProps) { ) : ( <> {/* Display a hint about who is editing the app */} - {blockEditing && ( + {blockEditing && Boolean(applicationId) && ( <> )} - {applicationId && ( + {Boolean(applicationId) && applicationId !== 'public' && ( { }, }); setAppInfo(info); + console.log(info); fetchJSDataSourceByApp(); setFetchingAppDetails(false); }, diff --git a/client/packages/lowcoder/src/pages/editor/AppEditorPublic.tsx b/client/packages/lowcoder/src/pages/editor/AppEditorPublic.tsx new file mode 100644 index 000000000..80b74df08 --- /dev/null +++ b/client/packages/lowcoder/src/pages/editor/AppEditorPublic.tsx @@ -0,0 +1,237 @@ +import { AppPathParams, AppTypeEnum } from "constants/applicationConstants"; +import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useParams } from "react-router"; +import { AppSummaryInfo, fetchApplicationInfo } from "redux/reduxActions/applicationActions"; +import { fetchDataSourceTypes } from "redux/reduxActions/datasourceActions"; +import { getUser } from "redux/selectors/usersSelectors"; +import { useUserViewMode } from "util/hooks"; +import "comps/uiCompRegistry"; +import { showAppSnapshotSelector } from "redux/selectors/appSnapshotSelector"; +import { setShowAppSnapshot } from "redux/reduxActions/appSnapshotActions"; +import { fetchGroupsAction } from "redux/reduxActions/orgActions"; +import { getFetchOrgGroupsFinished } from "redux/selectors/orgSelectors"; +import { getIsCommonSettingFetching } from "redux/selectors/commonSettingSelectors"; +import { + MarkAppDSLLoaded, + MarkAppEditorFirstRender, + MarkAppEditorMounted, + perfClear, + perfMark, +} from "util/perfUtils"; +import { useMount, useUnmount } from "react-use"; +import { clearGlobalSettings, setGlobalSettings } from "comps/utils/globalSettings"; +import { fetchFolderElements } from "redux/reduxActions/folderActions"; +import { registryDataSourcePlugin } from "constants/queryConstants"; +import { useRootCompInstance } from "./useRootCompInstance"; +import EditorSkeletonView from "./editorSkeletonView"; +import {ErrorBoundary} from 'react-error-boundary'; +import { ALL_APPLICATIONS_URL } from "@lowcoder-ee/constants/routesURL"; +import history from "util/history"; +import Flex from "antd/es/flex"; +import React from "react"; +import { currentApplication } from "@lowcoder-ee/redux/selectors/applicationSelector"; +import { AppState } from "@lowcoder-ee/redux/reducers"; +import { resetIconDictionary } from "@lowcoder-ee/constants/iconConstants"; +import {fetchJsDSPaginationByApp} from "@lowcoder-ee/util/pagination/axios"; + +const AppEditorInternalView = lazy( + () => import("pages/editor/appEditorInternal") + .then(moduleExports => ({default: moduleExports.AppEditorInternalView})) +); + +const AppEditorPublic = React.memo(() => { + const dispatch = useDispatch(); + const params = useParams(); + const isUserViewModeCheck = useUserViewMode(); + const showAppSnapshot = useSelector(showAppSnapshotSelector); + const currentUser = useSelector(getUser); + const fetchOrgGroupsFinished = useSelector(getFetchOrgGroupsFinished); + const isCommonSettingsFetching = useSelector(getIsCommonSettingFetching); + const application = useSelector(currentApplication); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [elements, setElements] = useState({ elements: [], total: 1 }) + const isLowcoderCompLoading = useSelector((state: AppState) => state.npmPlugin.loading.lowcoderComps); + + const isUserViewMode = useMemo( + () => params.viewMode ? isUserViewModeCheck : true, + [params.viewMode, isUserViewModeCheck] + ); + const applicationId = useMemo( + () => params.applicationId || window.location.pathname.split("/")[2], + [params.applicationId, window.location.pathname] + ); + console.log('viewMode', applicationId); + const paramViewMode = useMemo( + () => params.viewMode || window.location.pathname.split("/")[3], + [params.viewMode, window.location.pathname] + ); + const viewMode = useMemo( + () => (paramViewMode === "view" || paramViewMode === "admin") + ? "published" + : paramViewMode === "view_marketplace" ? "view_marketplace" : "editing", + [paramViewMode] + ); + + const firstRendered = useRef(false); + const orgId = useMemo(() => currentUser.currentOrgId, [currentUser.currentOrgId]); + const [isDataSourcePluginRegistered, setIsDataSourcePluginRegistered] = useState(false); + const [appError, setAppError] = useState(''); + const [blockEditing, setBlockEditing] = useState(true); + const [fetchingAppDetails, setFetchingAppDetails] = useState(false); + + setGlobalSettings({ applicationId, isViewMode: paramViewMode === "view" }); + + if (!firstRendered.current) { + perfClear(); + perfMark(MarkAppEditorFirstRender); + firstRendered.current = true; + } + + useMount(() => { + perfMark(MarkAppEditorMounted); + }); + + useUnmount(() => { + clearGlobalSettings(); + }); + + // fetch dsl + const [appInfo, setAppInfo] = useState({ + id: "", + appType: AppTypeEnum.Application, + }); + + const readOnly = applicationId === 'public' ? false : isUserViewMode; + console.log('readOnly', readOnly) + const compInstance = useRootCompInstance( + appInfo, + readOnly, + isDataSourcePluginRegistered, + blockEditing, + ); + + // fetch dataSource and plugin + useEffect(() => { + if (!orgId || paramViewMode !== "edit") { + return; + } + dispatch(fetchDataSourceTypes({ organizationId: orgId })); + dispatch(fetchFolderElements({})); + }, [dispatch, orgId, paramViewMode]); + + + const fetchJSDataSourceByApp = useCallback(() => { + fetchJsDSPaginationByApp({ + appId: applicationId, + pageNum: currentPage, + pageSize: pageSize + }).then((res) => { + setElements({elements: [], total: res.total || 1}) + res.data!.forEach((i: any) => { + registryDataSourcePlugin(i.type, i.id, i.pluginDefinition); + }); + setIsDataSourcePluginRegistered(true); + }); + dispatch(setShowAppSnapshot(false)); + }, [ + applicationId, + registryDataSourcePlugin, + setIsDataSourcePluginRegistered, + setShowAppSnapshot, + dispatch, + currentPage, + pageSize + ]); + + useEffect(() => { + if (!fetchOrgGroupsFinished) { + dispatch(fetchGroupsAction(orgId)); + } + }, [dispatch, fetchOrgGroupsFinished, orgId]); + + const fetchApplication = useCallback(() => { + setFetchingAppDetails(true); + dispatch( + fetchApplicationInfo({ + type: viewMode, + applicationId: applicationId, + onSuccess: (info) => { + perfMark(MarkAppDSLLoaded); + const runJsInHost = + info.orgCommonSettings?.runJavaScriptInHost ?? !!REACT_APP_DISABLE_JS_SANDBOX; + setGlobalSettings({ + orgCommonSettings: { + ...info.orgCommonSettings, + runJavaScriptInHost: runJsInHost, + }, + }); + console.log(info); + setAppInfo(info); + fetchJSDataSourceByApp(); + setFetchingAppDetails(false); + }, + onError: (errorMessage) => { + setAppError(errorMessage); + setFetchingAppDetails(false); + } + }) + ); + }, [viewMode, applicationId, dispatch, fetchJSDataSourceByApp]); + + useEffect(() => { + if(!isLowcoderCompLoading) { + fetchApplication(); + resetIconDictionary(); + } + }, [isLowcoderCompLoading, fetchApplication]); + + const fallbackUI = useMemo(() => ( + +

Something went wrong while displaying this webpage

+ +
+ ), []); + + if (Boolean(appError)) { + return ( + +

{appError}

+ +
+ ) + } + + return ( + + }> + {fetchingAppDetails + ? + : ( + + ) + } + + + ); +}); + +export default AppEditorPublic; diff --git a/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx b/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx index 09f9c4855..37db2e801 100644 --- a/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx +++ b/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx @@ -143,6 +143,7 @@ export const AppEditorInternalView = React.memo((props: AppEditorInternalViewPro const loading = !compInstance || !compInstance.comp || !compInstance.comp.preloaded || props.loading; + console.log('loading', loading); const currentUser = useSelector(getCurrentUser); return loading ? ( diff --git a/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts b/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts index a2d424787..347c5b392 100644 --- a/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts @@ -203,10 +203,132 @@ export function* publishApplicationSaga(action: ReduxAction) { try { - const response: AxiosResponse = yield call( - ApplicationApi.getApplicationDetail, - action.payload - ); + + let response: AxiosResponse; + if (action.payload.applicationId === 'public') { + response = { + status: 200, + statusText: 'OK', + headers: {}, + config: { + headers: {} as any, + }, + data: { + code: 1, + success: true, + message: "", + data: { + orgCommonSettings: undefined, + applicationDSL: { + "ui": { + "compType": "normal", + "comp": {} + }, + "refTree": { + "value": "" + }, + "hooks": [ + { + "compType": "urlParams", + "comp": {}, + "name": "url" + }, + { + "compType": "dayJsLib", + "comp": {}, + "name": "dayjs" + }, + { + "compType": "lodashJsLib", + "comp": {}, + "name": "_" + }, + { + "compType": "utils", + "comp": {}, + "name": "utils" + }, + { + "compType": "message", + "comp": {}, + "name": "message" + }, + { + "compType": "toast", + "comp": {}, + "name": "toast" + }, + { + "compType": "localStorage", + "comp": {}, + "name": "localStorage" + }, + { + "compType": "currentUser", + "comp": {}, + "name": "currentUser" + }, + { + "compType": "screenInfo", + "comp": {}, + "name": "screenInfo" + }, + { + "compType": "theme", + "comp": {}, + "name": "theme" + } + ], + "settings": { + "title": "", + "description": "", + "category": "Business", + "showHeaderInPublic": true, + "themeId": "default", + "preventAppStylesOverwriting": true, + "disableCollision": false, + "lowcoderCompVersion": "latest", + "maxWidth": { + "dropdown": "1920", + "input": 0 + }, + "gridRowCount": "Infinity", + "gridPaddingX": "20", + "gridPaddingY": "20" + }, + "preload": { + "script": "", + "css": "", + "globalCSS": "" + } + }, + moduleDSL: {}, + applicationInfoView: { + "orgId": "", + "applicationId": "public", + "name": "Public App", + "createAt": 1735651262539, + "createBy": "", + "role": "owner", + "applicationType": 1, + "applicationStatus": "NORMAL", + "folderId": '', + "lastViewTime": 0, + "lastModifyTime": 1735747724691, + "lastEditedAt": 1735737886323, + "folder": false, + "extra": {}, + "editingUserId": "", + }, + } + } + }; + } else { + response = yield call( + ApplicationApi.getApplicationDetail, + action.payload + ); + } const isValidResponse: boolean = doValidResponse(response); if (isValidResponse && action.payload) { const { diff --git a/client/packages/lowcoder/src/util/pagination/axios.ts b/client/packages/lowcoder/src/util/pagination/axios.ts index 42c0de270..fd42e2efd 100644 --- a/client/packages/lowcoder/src/util/pagination/axios.ts +++ b/client/packages/lowcoder/src/util/pagination/axios.ts @@ -6,11 +6,13 @@ import { fetchFolderRequestType, fetchGroupUserRequestType, fetchOrgsByEmailRequestType, fetchOrgUserRequestType, fetchQueryLibraryPaginationRequestType, + GenericApiPaginationResponse, orgGroupRequestType } from "@lowcoder-ee/util/pagination/type"; import OrgApi from "@lowcoder-ee/api/orgApi"; -import { DatasourceApi } from "@lowcoder-ee/api/datasourceApi"; +import { DatasourceApi, NodePluginDatasourceInfo } from "@lowcoder-ee/api/datasourceApi"; import {QueryLibraryApi} from "@lowcoder-ee/api/queryLibraryApi"; +import { AxiosResponse } from "axios"; export const fetchFolderElements = async (request: fetchFolderRequestType) => { try { @@ -135,7 +137,26 @@ export const fetchQLPaginationByOrg = async (request: fetchQueryLibraryPaginatio export const fetchJsDSPaginationByApp = async (request: fetchDataSourcePaginationRequestType)=> { try { - const response = await DatasourceApi.fetchJsDatasourcePaginationByApp(request); + let response: AxiosResponse, any>; + if (request.appId === 'public') { + response = { + status: 200, + statusText: 'OK', + headers: {}, + config: { + headers: {} as any, + }, + data: { + code: 1, + data: [], + message: "", + success: true, + total: 0, + } + }; + } else { + response = await DatasourceApi.fetchJsDatasourcePaginationByApp(request); + } return { success: true, data: response.data.data, From af394ac86d2ef906bf7b59b6c6dbf259dad1920f Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Mon, 6 Jan 2025 22:14:39 +0500 Subject: [PATCH 2/3] fixed datasrouces, npm plugings, js plugins for public app editor --- .../lowcoder/src/api/commonSettingApi.ts | 1 + .../src/components/JSLibraryModal.tsx | 4 + .../src/components/ResCreatePanel.tsx | 8 +- .../src/comps/comps/appSettingsComp.tsx | 18 ++- .../src/comps/comps/remoteComp/loaders.tsx | 6 +- .../lowcoder/src/constants/publicApp.ts | 136 ++++++++++++++++++ .../lowcoder/src/constants/routesURL.ts | 2 +- .../packages/lowcoder/src/i18n/locales/en.ts | 2 +- .../lowcoder/src/pages/common/header.tsx | 7 +- .../lowcoder/src/pages/common/help.tsx | 4 + .../src/pages/common/previewHeader.tsx | 13 +- .../src/pages/datasource/pluginPanel.tsx | 24 +++- .../lowcoder/src/pages/editor/AppEditor.tsx | 1 - .../src/pages/editor/AppEditorPublic.tsx | 25 ++-- .../src/pages/editor/appEditorInternal.tsx | 1 - .../lowcoder/src/pages/editor/editorView.tsx | 19 ++- .../src/pages/editor/right/ModulePanel.tsx | 46 +++--- .../editor/right/PluginPanel/PluginItem.tsx | 2 +- .../pages/editor/right/PluginPanel/index.tsx | 3 + .../src/redux/sagas/applicationSagas.ts | 121 +--------------- .../src/redux/sagas/commonSettingsSagas.ts | 11 ++ .../redux/selectors/applicationSelector.ts | 6 + .../lowcoder/src/util/pagination/axios.ts | 19 +-- 23 files changed, 275 insertions(+), 204 deletions(-) create mode 100644 client/packages/lowcoder/src/constants/publicApp.ts diff --git a/client/packages/lowcoder/src/api/commonSettingApi.ts b/client/packages/lowcoder/src/api/commonSettingApi.ts index 22ae54229..48d573fdd 100644 --- a/client/packages/lowcoder/src/api/commonSettingApi.ts +++ b/client/packages/lowcoder/src/api/commonSettingApi.ts @@ -24,6 +24,7 @@ export interface CommonSettingResponseData { export type SetCommonSettingPayload = { orgId: string; + isPublicApp?: boolean; data: { key: string; value: T; diff --git a/client/packages/lowcoder/src/components/JSLibraryModal.tsx b/client/packages/lowcoder/src/components/JSLibraryModal.tsx index edbc8f547..37b8b6efa 100644 --- a/client/packages/lowcoder/src/components/JSLibraryModal.tsx +++ b/client/packages/lowcoder/src/components/JSLibraryModal.tsx @@ -23,6 +23,7 @@ import { RecommendedJSLibraryMeta } from "api/jsLibraryApi"; import log from "loglevel"; import { TacoMarkDown } from "components/markdown"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; +import { isPublicApplication } from "@lowcoder-ee/redux/selectors/applicationSelector"; const ModalLabel = styled.div` display: flex; @@ -232,12 +233,15 @@ export function JSLibraryModal(props: JSLibraryModalProps) { const [url, setURL] = useState(""); const [urlError, setURLError] = useState(undefined); const [installError, setInstallError] = useState(undefined); + const isPublicApp = useSelector(isPublicApplication); const dispatch = useDispatch(); const recommends = useSelector(recommendJSLibrarySelector); useEffect(() => { + if (isPublicApp) return; + dispatch(fetchJSLibraryRecommendsAction()); }, [dispatch]); diff --git a/client/packages/lowcoder/src/components/ResCreatePanel.tsx b/client/packages/lowcoder/src/components/ResCreatePanel.tsx index 388927948..1dd4cfd2a 100644 --- a/client/packages/lowcoder/src/components/ResCreatePanel.tsx +++ b/client/packages/lowcoder/src/components/ResCreatePanel.tsx @@ -26,6 +26,7 @@ import { useSelector } from "react-redux"; import { getUser } from "../redux/selectors/usersSelectors"; import DataSourceIcon from "./DataSourceIcon"; import { genRandomKey } from "comps/utils/idGenerator"; +import { isPublicApplication } from "@lowcoder-ee/redux/selectors/applicationSelector"; const Wrapper = styled.div<{ $placement: PageType }>` width: 100%; @@ -232,6 +233,7 @@ export function ResCreatePanel(props: ResCreateModalProps) { const [isScrolling, setScrolling] = useState(false); const [visible, setVisible] = useState(false); + const isPublicApp = useSelector(isPublicApplication); const user = useSelector(getUser); const { width, ref } = useResizeDetector({ handleHeight: false }); @@ -289,7 +291,7 @@ export function ResCreatePanel(props: ResCreateModalProps) { onSelect={onSelect} /> - + {!isPublicApp && } ))} - {user.orgDev && ( + {(user.orgDev || isPublicApp) && ( setVisible(true)}> @@ -351,7 +353,7 @@ export function ResCreatePanel(props: ResCreateModalProps) { setVisible(false)} onCreated={() => setVisible(false)} /> diff --git a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx index a2b54ddc8..7749f262f 100644 --- a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx @@ -25,6 +25,7 @@ import type { AppState } from "@lowcoder-ee/redux/reducers"; import { ColorControl } from "../controls/colorControl"; import { DEFAULT_ROW_COUNT } from "@lowcoder-ee/layout/calculateUtils"; import { AppSettingContext } from "../utils/appSettingContext"; +import { isPublicApplication } from "@lowcoder-ee/redux/selectors/applicationSelector"; const TITLE = trans("appSetting.title"); const USER_DEFINE = "__USER_DEFINE"; @@ -337,6 +338,7 @@ function AppGeneralSettingsModal(props: ChildrenInstance) { } function AppCanvasSettingsModal(props: ChildrenInstance) { + const isPublicApp = useSelector(isPublicApplication); const { themeList, defaultTheme, @@ -415,13 +417,15 @@ function AppCanvasSettingsModal(props: ChildrenInstance) { placement="bottom" itemNode={(value) => } preNode={() => ( - <> - window.open(THEME_SETTING)}> - - {trans("appSetting.themeCreate")} - - - + isPublicApp ? <> : ( + <> + window.open(THEME_SETTING)}> + + {trans("appSetting.themeCreate")} + + + + ) )} allowClear onChange={(value) => { diff --git a/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx b/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx index 476227893..812842910 100644 --- a/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx +++ b/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx @@ -1,3 +1,4 @@ +import { PUBLIC_APP_ID } from "@lowcoder-ee/constants/publicApp"; import { sdkConfig } from "@lowcoder-ee/constants/sdkConfig"; import { ASSETS_BASE_URL, NPM_PLUGIN_ASSETS_BASE_URL } from "constants/npmPlugins"; import { trans } from "i18n"; @@ -23,8 +24,9 @@ async function npmLoader( ? `${sdkConfig.baseURL}/${ASSETS_BASE_URL}` : NPM_PLUGIN_ASSETS_BASE_URL; - const entry = `${pluginBaseUrl}/${appId || 'none'}/${packageName}@${localPackageVersion}/index.js`; - // const entry = `../../../../../public/package/index.js`; + const applicationId = (!appId || appId && appId === PUBLIC_APP_ID) ? 'none' : appId; + + const entry = `${pluginBaseUrl}/${applicationId}/${packageName}@${localPackageVersion}/index.js`; try { const module = await import( diff --git a/client/packages/lowcoder/src/constants/publicApp.ts b/client/packages/lowcoder/src/constants/publicApp.ts new file mode 100644 index 000000000..410d60475 --- /dev/null +++ b/client/packages/lowcoder/src/constants/publicApp.ts @@ -0,0 +1,136 @@ +export const PUBLIC_APP_ID = "public_app"; +export const PUBLIC_APP_ORG_ID = "645b53eb86b4c862d8ae0fb9"; + +export const publicAppResponse = { + status: 200, + statusText: 'OK', + headers: {}, + config: { + headers: {} as any, + }, + data: { + code: 1, + success: true, + message: "", + data: { + orgCommonSettings: undefined, + applicationDSL: { + "ui": { + "compType": "normal", + "comp": {} + }, + "refTree": { + "value": "" + }, + "hooks": [ + { + "compType": "urlParams", + "comp": {}, + "name": "url" + }, + { + "compType": "dayJsLib", + "comp": {}, + "name": "dayjs" + }, + { + "compType": "lodashJsLib", + "comp": {}, + "name": "_" + }, + { + "compType": "utils", + "comp": {}, + "name": "utils" + }, + { + "compType": "message", + "comp": {}, + "name": "message" + }, + { + "compType": "toast", + "comp": {}, + "name": "toast" + }, + { + "compType": "localStorage", + "comp": {}, + "name": "localStorage" + }, + { + "compType": "currentUser", + "comp": {}, + "name": "currentUser" + }, + { + "compType": "screenInfo", + "comp": {}, + "name": "screenInfo" + }, + { + "compType": "theme", + "comp": {}, + "name": "theme" + } + ], + "settings": { + "title": "", + "description": "", + "category": "Business", + "showHeaderInPublic": true, + "themeId": "default", + "preventAppStylesOverwriting": true, + "disableCollision": false, + "lowcoderCompVersion": "latest", + "maxWidth": { + "dropdown": "1920", + "input": 0 + }, + "gridRowCount": "Infinity", + "gridPaddingX": "20", + "gridPaddingY": "20" + }, + "preload": { + "script": "", + "css": "", + "globalCSS": "" + } + }, + moduleDSL: {}, + applicationInfoView: { + "orgId": "", + "applicationId": PUBLIC_APP_ID, + "name": "Public App", + "createAt": 1735651262539, + "createBy": "", + "role": "owner", + "applicationType": 1, + "applicationStatus": "NORMAL", + "folderId": '', + "lastViewTime": 0, + "lastModifyTime": 1735747724691, + "lastEditedAt": 1735737886323, + "folder": false, + "extra": {}, + "editingUserId": "", + }, + } + } +}; + +export const publicAppJSDatasourceResponse = { + status: 200, + statusText: 'OK', + headers: {}, + config: { + headers: {} as any, + }, + data: { + code: 1, + data: [], + message: "", + success: true, + total: 0, + } +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/constants/routesURL.ts b/client/packages/lowcoder/src/constants/routesURL.ts index 2e588aac4..6931a1d74 100644 --- a/client/packages/lowcoder/src/constants/routesURL.ts +++ b/client/packages/lowcoder/src/constants/routesURL.ts @@ -49,7 +49,7 @@ export const FOLDERS_URL = `/folders`; export const TRASH_URL = `/trash`; export const IMPORT_APP_FROM_TEMPLATE_URL = `${ALL_APPLICATIONS_URL}/template-import/:templateId`; export const APP_EDITOR_URL = `${ALL_APPLICATIONS_URL}/:applicationId/:viewMode/:appPageId?`; -export const PUBLIC_APP_EDITOR_URL = `${ALL_APPLICATIONS_URL}/public/edit`; +export const PUBLIC_APP_EDITOR_URL = `/editor/public`; export const AUTH_BIND_URL = `${USER_AUTH_URL}/bind`; export const AUTH_LOGIN_URL = `${USER_AUTH_URL}/login`; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 03153cbd5..3702a8503 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -139,7 +139,7 @@ export const en = { "pluginListTitle": "Plugins", "emptyModules": "Modules are reusable Mikro-Apps. You can embed them in your App.", "searchNotFound": "Can't find the right component?", - "emptyPlugins": "No Plugins Added", + "emptyPlugins": "No plugins added yet. Add npm plugins to enhance your project.", "contactUs": "Contact Us", "issueHere": "here.", "folderListTitle": "Folders" diff --git a/client/packages/lowcoder/src/pages/common/header.tsx b/client/packages/lowcoder/src/pages/common/header.tsx index 963ef753b..0b32ef396 100644 --- a/client/packages/lowcoder/src/pages/common/header.tsx +++ b/client/packages/lowcoder/src/pages/common/header.tsx @@ -40,7 +40,7 @@ import { recoverSnapshotAction, setShowAppSnapshot, } from "redux/reduxActions/appSnapshotActions"; -import { currentApplication } from "redux/selectors/applicationSelector"; +import { currentApplication, isPublicApplication } from "redux/selectors/applicationSelector"; import { getSelectedAppSnapshot, showAppSnapshotSelector, @@ -369,6 +369,7 @@ export default function Header(props: HeaderProps) { const { left, bottom, right } = props.panelStatus; const user = useSelector(getUser); const application = useSelector(currentApplication); + const isPublicApp = useSelector(isPublicApplication); const applicationId = useApplicationId(); const dispatch = useDispatch(); const showAppSnapshot = useSelector(showAppSnapshotSelector); @@ -383,8 +384,6 @@ export default function Header(props: HeaderProps) { const isModule = appType === AppTypeEnum.Module; - console.log('header', applicationId); - useEffect(() => { if(blockEditing && application && Boolean(application?.editingUserId)) { UserApi.getUserDetail(application.editingUserId!) @@ -589,7 +588,7 @@ export default function Header(props: HeaderProps) { )} - {Boolean(applicationId) && applicationId !== 'public' && ( + {Boolean(applicationId) && !isPublicApp && ( ` ${(props) => @@ -189,6 +190,8 @@ function HelpDropdownComp(props: HelpDropdownProps) { const [videoVisible, setVideoVisible] = useState(false); const [toolTipContent, setToolTipContent] = useState(null); const [showDropdown, setShowDropdown] = useState(false); + const isPublicApp = useSelector(isPublicApplication); + const closeTooltip = () => { // turn of tooltip setToolTipContent(null); @@ -238,6 +241,7 @@ function HelpDropdownComp(props: HelpDropdownProps) { !props.isEdit && setShowHelp(false); return; case "editorTutorial": + if (isPublicApp) return; dispatch( createApplication({ applicationName: trans("help.appName"), diff --git a/client/packages/lowcoder/src/pages/common/previewHeader.tsx b/client/packages/lowcoder/src/pages/common/previewHeader.tsx index 10afb9817..769d3c8fc 100644 --- a/client/packages/lowcoder/src/pages/common/previewHeader.tsx +++ b/client/packages/lowcoder/src/pages/common/previewHeader.tsx @@ -5,7 +5,7 @@ import { ALL_APPLICATIONS_URL, APPLICATION_VIEW_URL, AUTH_LOGIN_URL } from "cons import { User } from "constants/userConstants"; import { EllipsisTextCss, isDarkColor, TacoButton, TextEditIcon } from "lowcoder-design"; import { useSelector } from "react-redux"; -import { currentApplication, getTemplateId } from "redux/selectors/applicationSelector"; +import { currentApplication, getTemplateId, isPublicApplication } from "redux/selectors/applicationSelector"; import { getUser, isFetchingUser } from "redux/selectors/usersSelectors"; import styled from "styled-components"; import history from "util/history"; @@ -15,7 +15,7 @@ import ProfileDropdown from "./profileDropdown"; import { trans } from "i18n"; import { Logo } from "@lowcoder-ee/assets/images"; import { AppPermissionDialog } from "../../components/PermissionDialog/AppPermissionDialog"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { getBrandingConfig } from "../../redux/selectors/configSelectors"; import { HeaderStartDropdown } from "./headerStartDropdown"; import { useParams } from "react-router"; @@ -132,11 +132,12 @@ const PreviewHeaderComp = () => { const params = useParams(); const user = useSelector(getUser); const application = useSelector(currentApplication); + const isPublicApp = useSelector(isPublicApplication); const applicationId = useApplicationId(); const templateId = useSelector(getTemplateId); const brandingConfig = useSelector(getBrandingConfig); const [permissionDialogVisible, setPermissionDialogVisible] = useState(false); - const isViewMarketplaceMode = params.viewMode === 'view_marketplace'; + const isViewMarketplaceMode = params.viewMode === 'view_marketplace' || isPublicApp; const headerStart = ( <> @@ -159,17 +160,17 @@ const PreviewHeaderComp = () => { const headerEnd = ( - {canManageApp(user, application) && ( + {canManageApp(user, application) && !isPublicApp && ( !visible && setPermissionDialogVisible(false)} /> )} - {canManageApp(user, application) && ( + {canManageApp(user, application) && !isPublicApp && ( setPermissionDialogVisible(true)}>{SHARE_TITLE} )} - {canEditApp(user, application) && ( + {canEditApp(user, application) && !isPublicApp && ( diff --git a/client/packages/lowcoder/src/pages/datasource/pluginPanel.tsx b/client/packages/lowcoder/src/pages/datasource/pluginPanel.tsx index 38bd41c06..01a538154 100644 --- a/client/packages/lowcoder/src/pages/datasource/pluginPanel.tsx +++ b/client/packages/lowcoder/src/pages/datasource/pluginPanel.tsx @@ -14,6 +14,10 @@ import { import { Search } from "components/Search"; import { CreateDropdown } from "@lowcoder-ee/pages/ApplicationV2/CreateDropdown"; import React, { useState } from "react"; +import { isPublicApplication } from "@lowcoder-ee/redux/selectors/applicationSelector"; +import Alert from "antd/es/alert"; +import Flex from "antd/es/flex"; +import { Link } from "react-router-dom"; export const DataSourceButton = styled(AntdButton)` &&& { @@ -115,6 +119,7 @@ interface SectionProps { datasourceTypes: DataSourceTypeInfo[]; searchValue: string; onSelect: (t: DataSourceTypeInfo) => void; + isPublicApp: boolean; } const categories: Category[] = [ @@ -135,7 +140,7 @@ const categories: Category[] = [ ]; // Section component -const Section: React.FC = ({ label, filter, datasourceTypes, searchValue, onSelect }) => ( +const Section: React.FC = ({ label, filter, datasourceTypes, searchValue, onSelect, isPublicApp }) => ( {label} @@ -143,7 +148,7 @@ const Section: React.FC = ({ label, filter, datasourceTypes, searc .filter(filter) .filter((t) => localeContains(t.name, searchValue)) .map((t) => ( - onSelect(t)}> + onSelect(t)}> {t.id && getBottomResIcon(t.id, "large", t.definition?.icon)} {t.name} @@ -157,6 +162,7 @@ export const PluginPanel = (props: { onSelect: (t: DataSourceTypeInfo) => void } const currentPage = useCurrentPage(); const [searchValue, setSearchValue] = useState(""); const apiList = currentPage === "queryLibrary" ? apiPluginsForQueryLibrary : apiPlugins; + const isPublicApp = useSelector(isPublicApplication); return ( @@ -168,6 +174,19 @@ export const PluginPanel = (props: { onSelect: (t: DataSourceTypeInfo) => void } style={{ width: "192px", height: "32px", margin: "0" }} /> + {isPublicApp && ( + + You're currently in preview mode.  + Sign up +  now to unlock the full experience! + + } + /> + )} {categories.map(({ label, filter }) => (
void } datasourceTypes={datasourceTypes} searchValue={searchValue} onSelect={props.onSelect} + isPublicApp={isPublicApp} /> ))} diff --git a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx index 99fb3d30e..76183556d 100644 --- a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx +++ b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx @@ -191,7 +191,6 @@ const AppEditor = React.memo(() => { }, }); setAppInfo(info); - console.log(info); fetchJSDataSourceByApp(); setFetchingAppDetails(false); }, diff --git a/client/packages/lowcoder/src/pages/editor/AppEditorPublic.tsx b/client/packages/lowcoder/src/pages/editor/AppEditorPublic.tsx index 80b74df08..2afc07e9b 100644 --- a/client/packages/lowcoder/src/pages/editor/AppEditorPublic.tsx +++ b/client/packages/lowcoder/src/pages/editor/AppEditorPublic.tsx @@ -34,6 +34,7 @@ import { currentApplication } from "@lowcoder-ee/redux/selectors/applicationSele import { AppState } from "@lowcoder-ee/redux/reducers"; import { resetIconDictionary } from "@lowcoder-ee/constants/iconConstants"; import {fetchJsDSPaginationByApp} from "@lowcoder-ee/util/pagination/axios"; +import { PUBLIC_APP_ID, PUBLIC_APP_ORG_ID } from "@lowcoder-ee/constants/publicApp"; const AppEditorInternalView = lazy( () => import("pages/editor/appEditorInternal") @@ -59,23 +60,24 @@ const AppEditorPublic = React.memo(() => { [params.viewMode, isUserViewModeCheck] ); const applicationId = useMemo( - () => params.applicationId || window.location.pathname.split("/")[2], - [params.applicationId, window.location.pathname] + () => { + const appId = params.applicationId || window.location.pathname.split("/")[2]; + return appId === 'public' ? PUBLIC_APP_ID : appId; + }, [params.applicationId, window.location.pathname] ); - console.log('viewMode', applicationId); const paramViewMode = useMemo( () => params.viewMode || window.location.pathname.split("/")[3], [params.viewMode, window.location.pathname] ); const viewMode = useMemo( () => (paramViewMode === "view" || paramViewMode === "admin") - ? "published" - : paramViewMode === "view_marketplace" ? "view_marketplace" : "editing", + ? "published" + : paramViewMode === "view_marketplace" ? "view_marketplace" : "editing", [paramViewMode] ); const firstRendered = useRef(false); - const orgId = useMemo(() => currentUser.currentOrgId, [currentUser.currentOrgId]); + const orgId = PUBLIC_APP_ORG_ID; const [isDataSourcePluginRegistered, setIsDataSourcePluginRegistered] = useState(false); const [appError, setAppError] = useState(''); const [blockEditing, setBlockEditing] = useState(true); @@ -103,8 +105,8 @@ const AppEditorPublic = React.memo(() => { appType: AppTypeEnum.Application, }); - const readOnly = applicationId === 'public' ? false : isUserViewMode; - console.log('readOnly', readOnly) + const readOnly = applicationId === PUBLIC_APP_ID ? false : isUserViewMode; + const compInstance = useRootCompInstance( appInfo, readOnly, @@ -114,12 +116,8 @@ const AppEditorPublic = React.memo(() => { // fetch dataSource and plugin useEffect(() => { - if (!orgId || paramViewMode !== "edit") { - return; - } dispatch(fetchDataSourceTypes({ organizationId: orgId })); - dispatch(fetchFolderElements({})); - }, [dispatch, orgId, paramViewMode]); + }, [dispatch]); const fetchJSDataSourceByApp = useCallback(() => { @@ -167,7 +165,6 @@ const AppEditorPublic = React.memo(() => { runJavaScriptInHost: runJsInHost, }, }); - console.log(info); setAppInfo(info); fetchJSDataSourceByApp(); setFetchingAppDetails(false); diff --git a/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx b/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx index 37db2e801..09f9c4855 100644 --- a/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx +++ b/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx @@ -143,7 +143,6 @@ export const AppEditorInternalView = React.memo((props: AppEditorInternalViewPro const loading = !compInstance || !compInstance.comp || !compInstance.comp.preloaded || props.loading; - console.log('loading', loading); const currentUser = useSelector(getCurrentUser); return loading ? ( diff --git a/client/packages/lowcoder/src/pages/editor/editorView.tsx b/client/packages/lowcoder/src/pages/editor/editorView.tsx index d7521f2c2..bbdbfcb99 100644 --- a/client/packages/lowcoder/src/pages/editor/editorView.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorView.tsx @@ -42,7 +42,7 @@ import { Helmet } from "react-helmet"; import { useDispatch, useSelector } from "react-redux"; import { useLocation, useParams } from "react-router-dom"; import { setEditorExternalStateAction } from "redux/reduxActions/configActions"; -import { currentApplication } from "redux/selectors/applicationSelector"; +import { currentApplication, isPublicApplication } from "redux/selectors/applicationSelector"; import { showAppSnapshotSelector } from "redux/selectors/appSnapshotSelector"; import styled from "styled-components"; import { ExternalEditorContext } from "util/context/ExternalEditorContext"; @@ -304,6 +304,7 @@ function EditorView(props: EditorViewProps) { const editorState = useContext(EditorContext); const { readOnly, hideHeader } = useContext(ExternalEditorContext); const application = useSelector(currentApplication); + const isPublicApp = useSelector(isPublicApplication); const commonSettings = useSelector(getCommonSettings); const locationState = useLocation().state; const showNewUserGuide = locationState?.showNewUserGuide; @@ -506,11 +507,17 @@ function EditorView(props: EditorViewProps) { draggingUtils.clearData(); } } > -
+ {isPublicApp + ? + : ( +
+ ) + } {showNewUserGuide && } >): NodeType { - const elements = elementRecord[""]; + const elements = elementRecord[""] || []; const elementMap: Record = {}; let rootNode: NodeType = { name: "root", @@ -366,24 +367,6 @@ interface ModuleSidebarItemProps extends DraggableTreeNodeItemRenderProps { setSelectedType: (id: boolean) => void; } -const empty = ( - -

{trans("rightPanel.emptyModules")}

- { - const appId = app.applicationInfoView.applicationId; - const url = APPLICATION_VIEW_URL(appId, "edit"); - window.open(url); - }} - /> - - } - /> -); - function ModuleSidebarItem(props: ModuleSidebarItemProps) { const dispatch = useDispatch(); const { @@ -499,6 +482,7 @@ function ModuleSidebarItem(props: ModuleSidebarItemProps) { export default function ModulePanel() { const dispatch = useDispatch(); let elements = useSelector(folderElementsSelector); + const isPublicApp = useSelector(isPublicApplication); const { searchValue } = useContext(RightContext); const [selectedID, setSelectedID] = useState(""); const [selectedType, setSelectedType] = useState(false); @@ -510,8 +494,10 @@ export default function ModulePanel() { let popedItemSourceId = ""; useEffect(() => { + if (isPublicApp) return; + dispatch(fetchAllModules({})); - }, [dispatch]); + }, [dispatch, isPublicApp]); const moveModule = () => { try{ @@ -740,7 +726,25 @@ export default function ModulePanel() { /> ); }} - /> : empty} + /> : ( + +

{trans("rightPanel.emptyModules")}

+ {!isPublicApp && ( + { + const appId = app.applicationInfoView.applicationId; + const url = APPLICATION_VIEW_URL(appId, "edit"); + window.open(url); + }} + /> + )} + + } + /> + )} ); } diff --git a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginItem.tsx b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginItem.tsx index b9a8cfb66..88635a638 100644 --- a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginItem.tsx +++ b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginItem.tsx @@ -68,7 +68,7 @@ export function PluginItem(props: PluginViewProps) { useEffect(() => { setLoading(true); - axios.get(`${NPM_REGISTRY_URL}/${appId}/${name}`).then((res) => { + axios.get(`${NPM_REGISTRY_URL}/${appId || 'none'}/${name}`).then((res) => { if (res.status >= 400) { return; } diff --git a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx index ba408f92f..ea4781c75 100644 --- a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx +++ b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx @@ -11,6 +11,7 @@ import { getNpmPackageMeta, normalizeNpmPackage, validateNpmPackage } from "comp import { ComListTitle, ExtensionContentWrapper } from "../styledComponent"; import { EmptyContent } from "components/EmptyContent"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; +import { isPublicApplication } from "@lowcoder-ee/redux/selectors/applicationSelector"; const Footer = styled.div` display: flex; @@ -25,6 +26,7 @@ export default function PluginPanel() { const [newPluginName, setNewPluginName] = useState(""); const user = useSelector(getUser); const commonSettings = useSelector(getCommonSettings); + const isPublicApp = useSelector(isPublicApplication); const plugins = useMemo( () => @@ -40,6 +42,7 @@ export default function PluginPanel() { dispatch( setCommonSettings({ orgId: user.currentOrgId, + isPublicApp, data: { key: "npmPlugins", value: nextNpmPlugins, diff --git a/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts b/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts index 347c5b392..062f38145 100644 --- a/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts @@ -35,6 +35,7 @@ import { SERVER_ERROR_CODES } from "constants/apiConstants"; import history from "util/history"; import { ApplicationMeta, AppTypeEnum } from "constants/applicationConstants"; import { trans } from "i18n"; +import { PUBLIC_APP_ID, publicAppResponse } from "@lowcoder-ee/constants/publicApp"; export function* fetchHomeDataSaga(action: ReduxAction) { try { @@ -205,124 +206,8 @@ export function* fetchApplicationDetailSaga(action: ReduxAction; - if (action.payload.applicationId === 'public') { - response = { - status: 200, - statusText: 'OK', - headers: {}, - config: { - headers: {} as any, - }, - data: { - code: 1, - success: true, - message: "", - data: { - orgCommonSettings: undefined, - applicationDSL: { - "ui": { - "compType": "normal", - "comp": {} - }, - "refTree": { - "value": "" - }, - "hooks": [ - { - "compType": "urlParams", - "comp": {}, - "name": "url" - }, - { - "compType": "dayJsLib", - "comp": {}, - "name": "dayjs" - }, - { - "compType": "lodashJsLib", - "comp": {}, - "name": "_" - }, - { - "compType": "utils", - "comp": {}, - "name": "utils" - }, - { - "compType": "message", - "comp": {}, - "name": "message" - }, - { - "compType": "toast", - "comp": {}, - "name": "toast" - }, - { - "compType": "localStorage", - "comp": {}, - "name": "localStorage" - }, - { - "compType": "currentUser", - "comp": {}, - "name": "currentUser" - }, - { - "compType": "screenInfo", - "comp": {}, - "name": "screenInfo" - }, - { - "compType": "theme", - "comp": {}, - "name": "theme" - } - ], - "settings": { - "title": "", - "description": "", - "category": "Business", - "showHeaderInPublic": true, - "themeId": "default", - "preventAppStylesOverwriting": true, - "disableCollision": false, - "lowcoderCompVersion": "latest", - "maxWidth": { - "dropdown": "1920", - "input": 0 - }, - "gridRowCount": "Infinity", - "gridPaddingX": "20", - "gridPaddingY": "20" - }, - "preload": { - "script": "", - "css": "", - "globalCSS": "" - } - }, - moduleDSL: {}, - applicationInfoView: { - "orgId": "", - "applicationId": "public", - "name": "Public App", - "createAt": 1735651262539, - "createBy": "", - "role": "owner", - "applicationType": 1, - "applicationStatus": "NORMAL", - "folderId": '', - "lastViewTime": 0, - "lastModifyTime": 1735747724691, - "lastEditedAt": 1735737886323, - "folder": false, - "extra": {}, - "editingUserId": "", - }, - } - } - }; + if (action.payload.applicationId === PUBLIC_APP_ID) { + response = publicAppResponse as AxiosResponse; } else { response = yield call( ApplicationApi.getApplicationDetail, diff --git a/client/packages/lowcoder/src/redux/sagas/commonSettingsSagas.ts b/client/packages/lowcoder/src/redux/sagas/commonSettingsSagas.ts index 599c2b6bf..5e05367f4 100644 --- a/client/packages/lowcoder/src/redux/sagas/commonSettingsSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/commonSettingsSagas.ts @@ -39,6 +39,17 @@ export function* fetchCommonSettingsSaga(action: ReduxAction) { try { + // in case of public application, add data without sending request to BE + if (action.payload.isPublicApp) { + yield put({ + type: ReduxActionTypes.SET_COMMON_SETTING_SUCCESS, + payload: { + [action.payload.data.key]: action.payload.data.value, + }, + }); + return; + } + const response: AxiosResponse = yield call( CommonSettingApi.setCommonSetting, action.payload diff --git a/client/packages/lowcoder/src/redux/selectors/applicationSelector.ts b/client/packages/lowcoder/src/redux/selectors/applicationSelector.ts index 308543d5e..b33049e49 100644 --- a/client/packages/lowcoder/src/redux/selectors/applicationSelector.ts +++ b/client/packages/lowcoder/src/redux/selectors/applicationSelector.ts @@ -1,5 +1,6 @@ import { AppState } from "redux/reducers"; import { ApplicationMeta, AppPermissionInfo } from "constants/applicationConstants"; +import { PUBLIC_APP_ID } from "@lowcoder-ee/constants/publicApp"; export const normalAppListSelector = (state: AppState): ApplicationMeta[] => state.ui.application.applicationList.filter((app) => app.applicationStatus === "NORMAL"); @@ -47,3 +48,8 @@ export const getTemplateId = (state: AppState): any => { export const getServerSettings = (state: AppState): Record => { return state.ui.application.serverSettings || {}; } + +// an app to work on public editor +export const isPublicApplication = (state: AppState): boolean => { + return state.ui.application.currentApplication?.applicationId === PUBLIC_APP_ID +}; diff --git a/client/packages/lowcoder/src/util/pagination/axios.ts b/client/packages/lowcoder/src/util/pagination/axios.ts index fd42e2efd..d03bf1800 100644 --- a/client/packages/lowcoder/src/util/pagination/axios.ts +++ b/client/packages/lowcoder/src/util/pagination/axios.ts @@ -13,6 +13,7 @@ import OrgApi from "@lowcoder-ee/api/orgApi"; import { DatasourceApi, NodePluginDatasourceInfo } from "@lowcoder-ee/api/datasourceApi"; import {QueryLibraryApi} from "@lowcoder-ee/api/queryLibraryApi"; import { AxiosResponse } from "axios"; +import { PUBLIC_APP_ID, publicAppJSDatasourceResponse } from "@lowcoder-ee/constants/publicApp"; export const fetchFolderElements = async (request: fetchFolderRequestType) => { try { @@ -138,22 +139,8 @@ export const fetchQLPaginationByOrg = async (request: fetchQueryLibraryPaginatio export const fetchJsDSPaginationByApp = async (request: fetchDataSourcePaginationRequestType)=> { try { let response: AxiosResponse, any>; - if (request.appId === 'public') { - response = { - status: 200, - statusText: 'OK', - headers: {}, - config: { - headers: {} as any, - }, - data: { - code: 1, - data: [], - message: "", - success: true, - total: 0, - } - }; + if (request.appId === PUBLIC_APP_ID) { + response = publicAppJSDatasourceResponse; } else { response = await DatasourceApi.fetchJsDatasourcePaginationByApp(request); } From 96cbee6e813c64ec09533d79a75a4632c5acc9f9 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Mon, 6 Jan 2025 22:57:16 +0500 Subject: [PATCH 3/3] added feature to export public app --- .../src/pages/common/headerStartDropdown.tsx | 8 +++- .../src/pages/editor/appEditorInternal.tsx | 45 ++++++++++++++++++- .../src/util/context/ExternalEditorContext.ts | 2 + 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/headerStartDropdown.tsx b/client/packages/lowcoder/src/pages/common/headerStartDropdown.tsx index e11f88593..bb7d41b5a 100644 --- a/client/packages/lowcoder/src/pages/common/headerStartDropdown.tsx +++ b/client/packages/lowcoder/src/pages/common/headerStartDropdown.tsx @@ -13,7 +13,7 @@ import { trans, transToNode } from "i18n"; import { exportApplicationAsJSONFile } from "pages/ApplicationV2/components/AppImport"; import { useContext, useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { currentApplication } from "redux/selectors/applicationSelector"; +import { currentApplication, isPublicApplication } from "redux/selectors/applicationSelector"; import { showAppSnapshotSelector } from "redux/selectors/appSnapshotSelector"; import styled from "styled-components"; import history from "util/history"; @@ -74,9 +74,10 @@ export function HeaderStartDropdown(props: { setEdit: () => void, isViewMarketpl const showAppSnapshot = useSelector(showAppSnapshotSelector); const applicationId = useApplicationId(); const application = useSelector(currentApplication); + const isPublicApp = useSelector(isPublicApplication); const [showCopyModal, setShowCopyModal] = useState(false); const dispatch = useDispatch(); - const { appType } = useContext(ExternalEditorContext); + const { appType, exportPublicAppToJson } = useContext(ExternalEditorContext); const isModule = appType === AppTypeEnum.Module; const isEditable = canEditApp(user, application); @@ -137,6 +138,9 @@ export function HeaderStartDropdown(props: { setEdit: () => void, isViewMarketpl if (e.key === "edit") { props.setEdit(); } else if (e.key === "export") { + if (isPublicApp && exportPublicAppToJson) { + return exportPublicAppToJson?.(); + } exportApplicationAsJSONFile(applicationId); } else if (e.key === "duplicate") { setShowCopyModal(true); diff --git a/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx b/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx index 09f9c4855..8b006a504 100644 --- a/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx +++ b/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx @@ -1,7 +1,7 @@ import { AppSummaryInfo, updateApplication } from "redux/reduxActions/applicationActions"; import { useDispatch, useSelector } from "react-redux"; import { getExternalEditorState } from "redux/selectors/configSelectors"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { ExternalEditorContext, ExternalEditorContextState, @@ -26,6 +26,7 @@ import { RootCompInstanceType } from "./useRootCompInstance"; import { getCurrentUser } from "redux/selectors/usersSelectors"; import React from "react"; import { isEqual } from "lodash"; +import { isPublicApplication } from "@lowcoder-ee/redux/selectors/applicationSelector"; /** * FIXME: optimize the logic of saving comps @@ -88,6 +89,40 @@ function useSaveComp( }, [comp, applicationId, readOnly, dispatch]); } +const exportAppToJson = (appDSL?: any) => { + if (!appDSL) return; + + const id = `t--export-app-link`; + const existingLink = document.getElementById(id); + existingLink && existingLink.remove(); + const link = document.createElement("a"); + const time = new Date().getTime(); + + const applicationName = `test_app_${time}`; + const exportObj = { + applicationInfo: { + name: 'Test App', + createAt: time, + createBy: '', + applicationId: '', + applicationType: AppTypeEnum.Application, + }, + applicationDSL: appDSL, + }; + const blob = new Blob([JSON.stringify(exportObj)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + link.href = url; + link.download = applicationName + ".json"; + link.id = id; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + return; +} + interface AppEditorInternalViewProps { readOnly: boolean; blockEditing?: boolean; @@ -100,6 +135,7 @@ interface AppEditorInternalViewProps { export const AppEditorInternalView = React.memo((props: AppEditorInternalViewProps) => { const isUserViewMode = useUserViewMode(); const extraExternalEditorState = useSelector(getExternalEditorState); + const isPublicApp = useSelector(isPublicApplication); const dispatch = useDispatch(); const { readOnly, blockEditing, appInfo, compInstance, fetchApplication } = props; @@ -111,6 +147,11 @@ export const AppEditorInternalView = React.memo((props: AppEditorInternalViewPro appType: AppTypeEnum.Application, }); + const exportPublicAppToJson = useCallback(() => { + const appDsl = compInstance.comp?.toJsonValue(); + exportAppToJson(appDsl); + }, [compInstance.comp]) + useEffect(() => { setExternalEditorState((s) => ({ ...s, @@ -121,9 +162,11 @@ export const AppEditorInternalView = React.memo((props: AppEditorInternalViewPro hideHeader: window.location.pathname.split("/")[3] === "admin", blockEditing, fetchApplication: fetchApplication, + exportPublicAppToJson: isPublicApp ? exportPublicAppToJson : undefined, ...extraExternalEditorState, })); }, [ + exportPublicAppToJson, compInstance?.history, extraExternalEditorState, readOnly, diff --git a/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts b/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts index ab1ad3a5c..94ade722f 100644 --- a/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts +++ b/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts @@ -47,6 +47,8 @@ export interface ExternalEditorContextState { */ fetchApplication?: () => void; + exportPublicAppToJson?: () => void; + changeExternalState?: (state: Partial) => void; }