diff --git a/.gitignore b/.gitignore index 8758dff248..1dcb36aaa0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ application-dev-localhost.yaml server/api-service/lowcoder-server/src/main/resources/application-local-dev.yaml translations/locales/node_modules/ .vscode/settings.json +server/api-service/lowcoder-server/src/main/resources/application-local-dev-ee.yaml diff --git a/client/VERSION b/client/VERSION index fad066f801..4fd0fe3cd5 100644 --- a/client/VERSION +++ b/client/VERSION @@ -1 +1 @@ -2.5.0 \ No newline at end of file +2.5.1 \ No newline at end of file diff --git a/client/packages/lowcoder-comps/package.json b/client/packages/lowcoder-comps/package.json index 426370c03f..9570cd8958 100644 --- a/client/packages/lowcoder-comps/package.json +++ b/client/packages/lowcoder-comps/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-comps", - "version": "2.4.19", + "version": "2.5.1", "type": "module", "license": "MIT", "dependencies": { diff --git a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx index 5021ee5685..da34b56101 100644 --- a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx @@ -1,6 +1,7 @@ import { default as Form } from "antd/es/form"; import { default as Input } from "antd/es/input"; import { default as ColorPicker } from "antd/es/color-picker"; +import { default as Switch } from "antd/es/switch"; import { trans, getCalendarLocale } from "../../i18n/comps"; import { createRef, useContext, useRef, useState, useEffect, useCallback, useMemo, Suspense } from "react"; import dayjs from "dayjs"; @@ -11,14 +12,15 @@ import adaptivePlugin from "@fullcalendar/adaptive"; import dayGridPlugin from "@fullcalendar/daygrid"; import multiMonthPlugin from '@fullcalendar/multimonth'; import timeGridPlugin from "@fullcalendar/timegrid"; -import interactionPlugin from "@fullcalendar/interaction"; +import interactionPlugin, { EventResizeDoneArg } from "@fullcalendar/interaction"; import listPlugin from "@fullcalendar/list"; import allLocales from "@fullcalendar/core/locales-all"; -import { EventContentArg, DateSelectArg } from "@fullcalendar/core"; +import { EventContentArg, DateSelectArg, EventDropArg } from "@fullcalendar/core"; import momentPlugin from "@fullcalendar/moment"; import ErrorBoundary from "./errorBoundary"; import { default as Tabs } from "antd/es/tabs"; +import { differenceBy, differenceWith, isEqual, filter, includes } from "lodash"; import { isValidColor, @@ -54,6 +56,8 @@ import { migrateOldData, controlItem, depsConfig, + stateComp, + JSONObject, } from 'lowcoder-sdk'; import { @@ -79,6 +83,7 @@ import { resourceTimeGridHeaderToolbar, } from "./calendarConstants"; import { EventOptionControl } from "./eventOptionsControl"; +import { EventImpl } from "@fullcalendar/core/internal"; function fixOldData(oldData: any) { if(!Boolean(oldData)) return; @@ -196,6 +201,10 @@ let childrenMap: any = { currentPremiumView: dropdownControl(DefaultWithPremiumViewOptions, "resourceTimelineDay"), animationStyle: styleControl(AnimationStyle, 'animationStyle'), showVerticalScrollbar: withDefault(BoolControl, false), + initialData: stateComp({}), + updatedEvents: stateComp({}), + insertedEvents: stateComp({}), + deletedEvents: stateComp({}), }; // this should ensure backwards compatibility with older versions of the SDK @@ -233,8 +242,9 @@ let CalendarBasicComp = (function () { currentFreeView?: string; currentPremiumView?: string; animationStyle?:any; - modalStyle?:any - showVerticalScrollbar?:boolean + modalStyle?:any; + showVerticalScrollbar?:boolean; + initialData: Array; }, dispatch: any) => { const comp = useContext(EditorContext)?.getUICompByName( useContext(CompNameContext) @@ -243,11 +253,13 @@ let CalendarBasicComp = (function () { const theme = useContext(ThemeContext); const ref = createRef(); const editEvent = useRef(); + const initData = useRef(false); const [form] = Form.useForm(); const [left, setLeft] = useState(undefined); const [licensed, setLicensed] = useState(props.licenseKey !== ""); const [currentSlotLabelFormat, setCurrentSlotLabelFormat] = useState(slotLabelFormat); - + const [initDataMap, setInitDataMap] = useState>({}); + useEffect(() => { setLicensed(props.licenseKey !== ""); }, [props.licenseKey]); @@ -290,27 +302,53 @@ let CalendarBasicComp = (function () { start: dayjs(item.start, DateParser).format(), end: dayjs(item.end, DateParser).format(), allDay: item.allDay, - resourceId: item.resourceId ? item.resourceId : null, - groupId: item.groupId ? item.groupId : null, + ...(item.resourceId ? { resourceId: item.resourceId } : {}), + ...(item.groupId ? { groupId: item.groupId } : {}), backgroundColor: item.backgroundColor, - extendedProps: { - color: isValidColor(item.color || "") ? item.color : theme?.theme?.primary, - ...(item.groupId ? { groupId: item.groupId } : {}), // Ensure color is in extendedProps - detail: item.detail, - titleColor:item.titleColor, - detailColor:item.detailColor, - titleFontWeight:item.titleFontWeight, - titleFontStyle:item.titleFontStyle, - detailFontWeight:item.detailFontWeight, - detailFontStyle:item.detailFontStyle, - animation:item?.animation, - animationDelay:item?.animationDelay, - animationDuration:item?.animationDuration, - animationIterationCount:item?.animationIterationCount - }} + extendedProps: { // Ensure color is in extendedProps + color: isValidColor(item.color || "") ? item.color : theme?.theme?.primary, + detail: item.detail, + titleColor:item.titleColor, + detailColor:item.detailColor, + titleFontWeight:item.titleFontWeight, + titleFontStyle:item.titleFontStyle, + detailFontWeight:item.detailFontWeight, + detailFontStyle:item.detailFontStyle, + animation:item?.animation, + animationDelay:item?.animationDelay, + animationDuration:item?.animationDuration, + animationIterationCount:item?.animationIterationCount + } + } }) : [currentEvents]; }, [currentEvents, theme]) + useEffect(() => { + const mapData: Record = {}; + events?.forEach((item: any, index: number) => { + mapData[`${item.id}`] = index; + }) + + if (initData.current) { + const difference = differenceWith(events, props.initialData, isEqual); + const inserted = differenceBy(difference, Object.keys(initDataMap)?.map(id => ({ id })), 'id') + const updated = filter(difference, obj => includes(Object.keys(initDataMap), String(obj.id))); + const deleted = differenceBy(props.initialData, Object.keys(mapData)?.map(id => ({ id })), 'id') + + comp.children?.comp.children?.updatedEvents.dispatchChangeValueAction(updated); + comp.children?.comp.children?.insertedEvents.dispatchChangeValueAction(inserted); + comp.children?.comp.children?.deletedEvents.dispatchChangeValueAction(deleted); + } + + if (!initData.current && events?.length && comp?.children?.comp?.children?.initialData) { + setInitDataMap(mapData); + comp?.children?.comp?.children?.initialData?.dispatch?.( + comp?.children?.comp?.children?.initialData?.changeValueAction?.([...events]) + ); + initData.current = true; + } + }, [JSON.stringify(events), comp?.children?.comp?.children?.initialData]); + const resources = useMemo(() => props.resources.value, [props.resources.value]); // list all plugins for Fullcalendar @@ -370,12 +408,12 @@ let CalendarBasicComp = (function () { }, [slotLabelFormat, slotLabelFormatWeek, slotLabelFormatMonth]); const handleEventDataChange = useCallback((data: Array>) => { - comp.children?.comp.children.events.children.manual.children.manual.dispatch( - comp.children?.comp.children.events.children.manual.children.manual.setChildrensAction( + comp?.children?.comp.children.events.children.manual.children.manual.dispatch( + comp?.children?.comp.children.events.children.manual.children.manual.setChildrensAction( data ) ); - comp.children?.comp.children.events.children.mapData.children.data.dispatchChangeValueAction( + comp?.children?.comp.children.events.children.mapData.children.data.dispatchChangeValueAction( JSON.stringify(data) ); props.onEvent("change"); @@ -506,6 +544,24 @@ let CalendarBasicComp = (function () { > + + + + + + + + + @@ -768,12 +824,35 @@ let CalendarBasicComp = (function () { showModal(event, false); }, [editEvent, showModal]); - const handleDrop = useCallback(() => { + const updateEventsOnDragOrResize = useCallback((eventInfo: EventImpl) => { + const {extendedProps, title, ...event} = eventInfo.toJSON(); + + let eventsList = [...props.events]; + const eventIdx = eventsList.findIndex( + (item: EventType) => item.id === event.id + ); + if (eventIdx > -1) { + eventsList[eventIdx] = { + label: title, + ...event, + ...extendedProps, + }; + handleEventDataChange(eventsList); + } + }, [props.events, handleEventDataChange]); + + const handleDrop = useCallback((eventInfo: EventDropArg) => { + updateEventsOnDragOrResize(eventInfo.event); + if (typeof props.onDropEvent === 'function') { - props.onDropEvent("dropEvent"); + props.onDropEvent("drop"); } - }, [props.onDropEvent]); - + }, [props.onDropEvent, updateEventsOnDragOrResize]); + + const handleResize = useCallback((eventInfo: EventResizeDoneArg) => { + updateEventsOnDragOrResize(eventInfo.event); + }, [props.onDropEvent, updateEventsOnDragOrResize]); + return ( { - if (info.view) { - handleDrop(); + eventDragStart={() => { + if (typeof props.onDropEvent === 'function') { + props.onDropEvent("drag"); } }} + eventDrop={handleDrop} + eventResize={handleResize} /> @@ -1007,6 +1088,30 @@ const TmpCalendarComp = withExposingConfigs(CalendarBasicComp, [ return input.events.filter(event => Boolean(event.resourceId)); }, }), + depsConfig({ + name: "toUpdatedEvents", + desc: trans("calendar.updatedEvents"), + depKeys: ["updatedEvents"], + func: (input: { updatedEvents: any[]; }) => { + return input.updatedEvents; + }, + }), + depsConfig({ + name: "toInsertedEvents", + desc: trans("calendar.insertedEvents"), + depKeys: ["insertedEvents"], + func: (input: { insertedEvents: any[]; }) => { + return input.insertedEvents; + }, + }), + depsConfig({ + name: "toDeletedEvents", + desc: trans("calendar.deletedEvents"), + depKeys: ["deletedEvents"], + func: (input: { deletedEvents: any[]; }) => { + return input.deletedEvents; + }, + }), ]); let CalendarComp = withMethodExposing(TmpCalendarComp, [ @@ -1124,7 +1229,43 @@ let CalendarComp = withMethodExposing(TmpCalendarComp, [ const viewKey = comp.children.licenseKey.getView() === "" ? 'defaultFreeView' : 'defaultPremiumView'; comp.children["viewKey"].dispatchChangeValueAction("multiMonthYear"); } - } + }, + { + method: { + name: "clearUpdatedEvents", + detail: "Clear updated events list", + params: [], + }, + execute: (comp) => { + comp?.children?.updatedEvents.dispatch( + comp?.children?.updatedEvents.changeValueAction([]) + ); + } + }, + { + method: { + name: "clearInsertedEvents", + detail: "Clear inserted events list", + params: [], + }, + execute: (comp) => { + comp?.children?.insertedEvents.dispatch( + comp?.children?.insertedEvents.changeValueAction([]) + ); + } + }, + { + method: { + name: "clearDeletedEvents", + detail: "Clear deleted events list", + params: [], + }, + execute: (comp) => { + comp?.children?.deletedEvents.dispatch( + comp?.children?.deletedEvents.changeValueAction([]) + ); + } + }, ]); diff --git a/client/packages/lowcoder-comps/src/i18n/comps/locales/en.ts b/client/packages/lowcoder-comps/src/i18n/comps/locales/en.ts index f6261b84ff..f8d5f77f72 100644 --- a/client/packages/lowcoder-comps/src/i18n/comps/locales/en.ts +++ b/client/packages/lowcoder-comps/src/i18n/comps/locales/en.ts @@ -271,6 +271,9 @@ export const en = { resourcesDefault: "Rooms", resourcesName: "Resource Name", resourcesEvents : "Resources Events Data", + deletedEvents : "List of deleted events", + updatedEvents : "List of updated events", + insertedEvents : "List of inserted events", editable: "Editable", license: "Licence Key", licenseTooltip: "Get your licence key from https://fullcalendar.io/purchase to enable premium views like Resource Timeline and Resource Grid.", diff --git a/client/packages/lowcoder-core/lib/index.cjs b/client/packages/lowcoder-core/lib/index.cjs index 95905706eb..7b061c66d2 100644 --- a/client/packages/lowcoder-core/lib/index.cjs +++ b/client/packages/lowcoder-core/lib/index.cjs @@ -9,118 +9,118 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau var ___default = /*#__PURE__*/_interopDefaultLegacy(_); -/****************************************************************************** -Copyright (c) Microsoft Corporation. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -***************************************************************************** */ -/* global Reflect, Promise, SuppressedError, Symbol */ - -var extendStatics = function(d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; - return extendStatics(d, b); -}; - -function __extends(d, b) { - if (typeof b !== "function" && b !== null) - throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); -} - -var __assign = function() { - __assign = Object.assign || function __assign(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; - -function __rest(s, e) { - var t = {}; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) - t[p] = s[p]; - if (s != null && typeof Object.getOwnPropertySymbols === "function") - for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { - if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) - t[p[i]] = s[p[i]]; - } - return t; -} - -function __decorate(decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -} - -function __awaiter(thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -} - -function __generator(thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -} - -function __spreadArray(to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -} - -typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { - var e = new Error(message); - return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; +/****************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +/* global Reflect, Promise, SuppressedError, Symbol */ + +var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); +}; + +function __extends(d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +} + +var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; + +function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +} + +function __decorate(decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +} + +function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +} + +function __generator(thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +} + +function __spreadArray(to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +} + +typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { + var e = new Error(message); + return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; function isEqualArgs(args, cacheArgs, equals) { @@ -1789,7 +1789,7 @@ var CodeNode = /** @class */ (function (_super) { if ((pathsArr === null || pathsArr === void 0 ? void 0 : pathsArr[0]) === (options === null || options === void 0 ? void 0 : options.queryName)) return; // wait for lazy loaded comps to load before executing query on page load - if (!Object.keys(value).length && paths.size) { + if (value && !Object.keys(value).length && paths.size) { isFetching_1 = true; ready_1 = false; } diff --git a/client/packages/lowcoder-core/lib/index.js b/client/packages/lowcoder-core/lib/index.js index 28dc7a0756..66045110c0 100644 --- a/client/packages/lowcoder-core/lib/index.js +++ b/client/packages/lowcoder-core/lib/index.js @@ -1,118 +1,118 @@ import _ from 'lodash'; import { serialize, compile, middleware, prefixer, stringify } from 'stylis'; -/****************************************************************************** -Copyright (c) Microsoft Corporation. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -***************************************************************************** */ -/* global Reflect, Promise, SuppressedError, Symbol */ - -var extendStatics = function(d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; - return extendStatics(d, b); -}; - -function __extends(d, b) { - if (typeof b !== "function" && b !== null) - throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); -} - -var __assign = function() { - __assign = Object.assign || function __assign(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; - -function __rest(s, e) { - var t = {}; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) - t[p] = s[p]; - if (s != null && typeof Object.getOwnPropertySymbols === "function") - for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { - if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) - t[p[i]] = s[p[i]]; - } - return t; -} - -function __decorate(decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -} - -function __awaiter(thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -} - -function __generator(thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -} - -function __spreadArray(to, from, pack) { - if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { - if (ar || !(i in from)) { - if (!ar) ar = Array.prototype.slice.call(from, 0, i); - ar[i] = from[i]; - } - } - return to.concat(ar || Array.prototype.slice.call(from)); -} - -typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { - var e = new Error(message); - return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; +/****************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +/* global Reflect, Promise, SuppressedError, Symbol */ + +var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); +}; + +function __extends(d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +} + +var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; + +function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +} + +function __decorate(decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +} + +function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +} + +function __generator(thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +} + +function __spreadArray(to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +} + +typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { + var e = new Error(message); + return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; function isEqualArgs(args, cacheArgs, equals) { @@ -1781,7 +1781,7 @@ var CodeNode = /** @class */ (function (_super) { if ((pathsArr === null || pathsArr === void 0 ? void 0 : pathsArr[0]) === (options === null || options === void 0 ? void 0 : options.queryName)) return; // wait for lazy loaded comps to load before executing query on page load - if (!Object.keys(value).length && paths.size) { + if (value && !Object.keys(value).length && paths.size) { isFetching_1 = true; ready_1 = false; } diff --git a/client/packages/lowcoder-core/src/eval/codeNode.tsx b/client/packages/lowcoder-core/src/eval/codeNode.tsx index f5d31cd7fa..2b67e7bbfa 100644 --- a/client/packages/lowcoder-core/src/eval/codeNode.tsx +++ b/client/packages/lowcoder-core/src/eval/codeNode.tsx @@ -177,7 +177,7 @@ export class CodeNode extends AbstractNode> { if (pathsArr?.[0] === options?.queryName) return; // wait for lazy loaded comps to load before executing query on page load - if (!Object.keys(value).length && paths.size) { + if (value && !Object.keys(value).length && paths.size) { isFetching = true; ready = false; } diff --git a/client/packages/lowcoder-design/src/components/Search.tsx b/client/packages/lowcoder-design/src/components/Search.tsx index 11e5f2adcb..dff0ebeeb9 100644 --- a/client/packages/lowcoder-design/src/components/Search.tsx +++ b/client/packages/lowcoder-design/src/components/Search.tsx @@ -62,24 +62,35 @@ interface ISearch { placeholder: string; value: string; onChange: (value: React.ChangeEvent) => void; + onEnterPress?: (value: string) => void; // Added for capturing Enter key press disabled?: boolean; } export const Search = (props: ISearch & InputProps) => { - const { value, onChange, style, disabled, placeholder, ...others } = props; + const { value, onChange, style, disabled, placeholder, onEnterPress, ...others } = props; + const handleChange = (e: React.ChangeEvent) => { onChange && onChange(e); }; + + // Handling Enter key press + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && onEnterPress) { + onEnterPress(value); + } + }; + return ( - - } - {...others} - /> - + + } + {...others} + /> + ); -}; +}; \ No newline at end of file diff --git a/client/packages/lowcoder-design/src/icons/index.tsx b/client/packages/lowcoder-design/src/icons/index.tsx index 687d3516b3..a538cb9bbb 100644 --- a/client/packages/lowcoder-design/src/icons/index.tsx +++ b/client/packages/lowcoder-design/src/icons/index.tsx @@ -1,4 +1,5 @@ export { ReactComponent as AppSnapshotIcon } from "./v1/app-snapshot.svg"; +export { ReactComponent as ArchiveIcon } from "./remix/archive-fill.svg"; export { ReactComponent as HookCompDropIcon } from "./v1/hook-comp-drop.svg"; export { ReactComponent as HookCompIcon } from "./v1/hook-comp.svg"; diff --git a/client/packages/lowcoder-sdk/package.json b/client/packages/lowcoder-sdk/package.json index ed81c3ba74..8b621a8e8e 100644 --- a/client/packages/lowcoder-sdk/package.json +++ b/client/packages/lowcoder-sdk/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-sdk", - "version": "2.4.16", + "version": "2.5.1", "type": "module", "files": [ "src", diff --git a/client/packages/lowcoder/index.html b/client/packages/lowcoder/index.html index f3019a0cd4..b9f940e010 100644 --- a/client/packages/lowcoder/index.html +++ b/client/packages/lowcoder/index.html @@ -28,6 +28,8 @@ display: flex; pointer-events: none; flex-direction: column; + top: 0; + z-index: 10000; } #loading svg { animation: breath 1s linear infinite; diff --git a/client/packages/lowcoder/package.json b/client/packages/lowcoder/package.json index d520a927d9..379d2c8ed1 100644 --- a/client/packages/lowcoder/package.json +++ b/client/packages/lowcoder/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder", - "version": "2.3.1", + "version": "2.5.1", "private": true, "type": "module", "main": "src/index.sdk.ts", diff --git a/client/packages/lowcoder/src/api/appSnapshotApi.ts b/client/packages/lowcoder/src/api/appSnapshotApi.ts index 18e98678e3..572576605c 100644 --- a/client/packages/lowcoder/src/api/appSnapshotApi.ts +++ b/client/packages/lowcoder/src/api/appSnapshotApi.ts @@ -22,18 +22,34 @@ export interface AppSnapshotDslResp extends ApiResponse { class AppSnapshotApi extends Api { static createSnapshotURL = "/application/history-snapshots"; static snapshotsURL = (appId: string) => `/application/history-snapshots/${appId}`; + static archiveSnapshotsURL = (appId: string) => `/application/history-snapshots/archive/${appId}`; static snapshotDslURL = (appId: string, snapshotId: string) => `/application/history-snapshots/${appId}/${snapshotId}`; - + static archiveSnapshotDslURL = (appId: string, snapshotId: string) => + `/application/history-snapshots/archive/${appId}/${snapshotId}`; static createSnapshot(request: CreateSnapshotPayload): AxiosPromise { return Api.post(AppSnapshotApi.createSnapshotURL, request); } - static getSnapshots(appId: string, pagination: PaginationParam): AxiosPromise { + static getSnapshots( + appId: string, + pagination: PaginationParam, + archived?: boolean, + ): AxiosPromise { + if (archived) { + return Api.get(AppSnapshotApi.archiveSnapshotsURL(appId), pagination); + } return Api.get(AppSnapshotApi.snapshotsURL(appId), pagination); } - static getSnapshotDsl(appId: string, snapshotId: string): AxiosPromise { + static getSnapshotDsl( + appId: string, + snapshotId: string, + archived?: boolean, + ): AxiosPromise { + if (archived) { + return Api.get(AppSnapshotApi.archiveSnapshotDslURL(appId, snapshotId)); + } return Api.get(AppSnapshotApi.snapshotDslURL(appId, snapshotId)); } } diff --git a/client/packages/lowcoder/src/api/applicationApi.ts b/client/packages/lowcoder/src/api/applicationApi.ts index a0edb74243..2411b50d80 100644 --- a/client/packages/lowcoder/src/api/applicationApi.ts +++ b/client/packages/lowcoder/src/api/applicationApi.ts @@ -12,7 +12,7 @@ import { SetAppEditingStatePayload, UpdateAppPermissionPayload, } from "redux/reduxActions/applicationActions"; -import { ApiResponse, GenericApiResponse } from "./apiResponses"; +import {ApiResponse, GenericApiResponse} from "./apiResponses"; import { JSONObject, JSONValue } from "util/jsonTypes"; import { ApplicationDetail, @@ -24,6 +24,7 @@ import { } from "constants/applicationConstants"; import { CommonSettingResponseData } from "./commonSettingApi"; import { ResourceType } from "@lowcoder-ee/constants/queryConstants"; +import {fetchAppRequestType, GenericApiPaginationResponse} from "@lowcoder-ee/util/pagination/type"; export interface HomeOrgMeta { id: string; @@ -108,6 +109,10 @@ class ApplicationApi extends Api { return Api.get(ApplicationApi.newURLPrefix + "/list", { ...request, withContainerSize: false }); } + static fetchAllApplicationsPagination(request: fetchAppRequestType): AxiosPromise> { + return Api.get(ApplicationApi.newURLPrefix + "/list", { ...request, withContainerSize: false, applicationStatus: "RECYCLED" }); + } + static fetchAllModules(request: HomeDataPayload): AxiosPromise { return Api.get(ApplicationApi.newURLPrefix + "/list", { applicationType: AppTypeEnum.Module, diff --git a/client/packages/lowcoder/src/api/datasourceApi.ts b/client/packages/lowcoder/src/api/datasourceApi.ts index ea08bb9346..1be29e6469 100644 --- a/client/packages/lowcoder/src/api/datasourceApi.ts +++ b/client/packages/lowcoder/src/api/datasourceApi.ts @@ -8,6 +8,11 @@ import { JSONArray } from "util/jsonTypes"; import { AuthType, HttpOAuthGrantType } from "pages/datasource/form/httpDatasourceForm"; import { Datasource } from "@lowcoder-ee/constants/datasourceConstants"; import { DataSourcePluginMeta } from "lowcoder-sdk/dataSource"; +import { + fetchDataSourcePaginationRequestType, + fetchDBRequestType, + GenericApiPaginationResponse +} from "@lowcoder-ee/util/pagination/type"; export interface PreparedStatementConfig { enableTurnOffPreparedStatement: boolean; @@ -164,6 +169,11 @@ export class DatasourceApi extends Api { return Api.get(DatasourceApi.url + `/jsDatasourcePlugins?appId=${appId}`); } + static fetchJsDatasourcePaginationByApp( request: fetchDataSourcePaginationRequestType ): AxiosPromise> { + const {appId, ...res} = request + return Api.get(DatasourceApi.url + `/jsDatasourcePlugins?appId=${appId}` ,{...res}); + } + static fetchDatasourceByApp(appId: string): AxiosPromise> { return Api.get(DatasourceApi.url + `/listByApp?appId=${appId}`); } @@ -172,6 +182,11 @@ export class DatasourceApi extends Api { return Api.get(DatasourceApi.url + `/listByOrg?orgId=${orgId}`); } + static fetchDatasourcePaginationByOrg(request: fetchDBRequestType): AxiosPromise> { + const {orgId, ...res} = request; + return Api.get(DatasourceApi.url + `/listByOrg?orgId=${orgId}`, {...res}); + } + static createDatasource( datasourceConfig: Partial ): AxiosPromise> { diff --git a/client/packages/lowcoder/src/api/folderApi.ts b/client/packages/lowcoder/src/api/folderApi.ts index 0f2fd47e59..113bab0462 100644 --- a/client/packages/lowcoder/src/api/folderApi.ts +++ b/client/packages/lowcoder/src/api/folderApi.ts @@ -9,6 +9,10 @@ import { UpdateFolderPayload, } from "../redux/reduxActions/folderActions"; import { ApplicationMeta, FolderMeta } from "../constants/applicationConstants"; +import { + fetchFolderRequestType, + GenericApiPaginationResponse +} from "@lowcoder-ee/util/pagination/type"; export class FolderApi extends Api { static url = "/folders"; @@ -40,4 +44,11 @@ export class FolderApi extends Api { ): AxiosPromise> { return Api.get(FolderApi.url + `/elements`, { id: request.folderId }); } + + static fetchFolderElementsPagination( + request: fetchFolderRequestType + ): AxiosPromise> { + const {id, ...res} = request + return request.id ? Api.get(FolderApi.url + `/elements`,{id: id, ...res}) : Api.get(FolderApi.url + `/elements`, { ...request }); + } } diff --git a/client/packages/lowcoder/src/api/orgApi.ts b/client/packages/lowcoder/src/api/orgApi.ts index 6e7c532e4d..588a20df51 100644 --- a/client/packages/lowcoder/src/api/orgApi.ts +++ b/client/packages/lowcoder/src/api/orgApi.ts @@ -10,6 +10,15 @@ import { UpdateUserOrgRolePayload, } from "redux/reduxActions/orgActions"; import { ApiResponse, GenericApiResponse } from "./apiResponses"; +import { + ApiPaginationResponse, + fetchGroupUserRequestType, + fetchOrgsByEmailRequestType, + fetchOrgUserRequestType, + GenericApiPaginationResponse, + GroupUsersPaginationResponse, + orgGroupRequestType, OrgUsersPaginationResponse +} from "@lowcoder-ee/util/pagination/type"; export interface GroupUsersResponse extends ApiResponse { data: { @@ -66,6 +75,10 @@ export class OrgApi extends Api { return Api.get(OrgApi.fetchGroupURL); } + static fetchGroupPagination(request: orgGroupRequestType): AxiosPromise> { + return Api.get(OrgApi.fetchGroupURL, {...request}); + } + static deleteGroup(groupId: string): AxiosPromise { return Api.delete(OrgApi.deleteGroupURL(groupId)); } @@ -88,10 +101,20 @@ export class OrgApi extends Api { return Api.get(OrgApi.fetchOrgUsersURL(orgId)); } + static fetchOrgUsersPagination(request:fetchOrgUserRequestType): AxiosPromise { + const {orgId, ...res} = request; + return Api.get(OrgApi.fetchOrgUsersURL(orgId), {...res}); + } + static fetchGroupUsers(groupId: string): AxiosPromise { return Api.get(OrgApi.fetchGroupUsersURL(groupId)); } + static fetchGroupUsersPagination(request: fetchGroupUserRequestType): AxiosPromise { + const {groupId, ...res} = request; + return Api.get(OrgApi.fetchGroupUsersURL(groupId), {...res}); + } + static deleteGroupUser(request: RemoveGroupUserPayload): AxiosPromise { return Api.delete(OrgApi.deleteGroupUserURL(request.groupId), { userId: request.userId, @@ -145,6 +168,11 @@ export class OrgApi extends Api { static fetchOrgsByEmail(email: string): AxiosPromise { return Api.get(OrgApi.fetchOrgsByEmailURL(email)); } + + static fetchOrgsPaginationByEmail(request: fetchOrgsByEmailRequestType): AxiosPromise { + const { email, ...rest } = request; + return Api.get(OrgApi.fetchOrgsByEmailURL(email), {...rest}); + } } export default OrgApi; diff --git a/client/packages/lowcoder/src/api/queryLibraryApi.ts b/client/packages/lowcoder/src/api/queryLibraryApi.ts index 063cf6ecca..16e6a9dc0d 100644 --- a/client/packages/lowcoder/src/api/queryLibraryApi.ts +++ b/client/packages/lowcoder/src/api/queryLibraryApi.ts @@ -2,6 +2,7 @@ import Api from "./api"; import { AxiosPromise } from "axios"; import { GenericApiResponse } from "./apiResponses"; import { DatasourceType } from "@lowcoder-ee/constants/queryConstants"; +import {fetchQueryLibraryPaginationRequestType, GenericApiPaginationResponse} from "@lowcoder-ee/util/pagination/type"; export interface LibraryQuery { id: string; @@ -49,6 +50,10 @@ export class QueryLibraryApi extends Api { return Api.get(QueryLibraryApi.url + `/listByOrg`); } + static fetchQueryLibraryPaginationByOrg(request: fetchQueryLibraryPaginationRequestType): AxiosPromise>> { + return Api.get(QueryLibraryApi.url + `/listByOrg`, {...request}); + } + static fetchQueryLibraryDropdown(): AxiosPromise< GenericApiResponse> > { diff --git a/client/packages/lowcoder/src/components/DraggableTree/DraggableItem.tsx b/client/packages/lowcoder/src/components/DraggableTree/DraggableItem.tsx index c8a0f093e7..4d827381c8 100644 --- a/client/packages/lowcoder/src/components/DraggableTree/DraggableItem.tsx +++ b/client/packages/lowcoder/src/components/DraggableTree/DraggableItem.tsx @@ -15,7 +15,7 @@ const Wrapper = styled.div<{ $itemHeight?: number; }>` position: relative; - width: 100%; + width: auto; height: ${(props) => props.$itemHeight ?? 30}px; /* border: 1px solid #d7d9e0; */ border-radius: 4px; diff --git a/client/packages/lowcoder/src/components/DraggableTree/DroppableMenuItem.tsx b/client/packages/lowcoder/src/components/DraggableTree/DroppableMenuItem.tsx index 68c355ec37..7c9eac729f 100644 --- a/client/packages/lowcoder/src/components/DraggableTree/DroppableMenuItem.tsx +++ b/client/packages/lowcoder/src/components/DraggableTree/DroppableMenuItem.tsx @@ -6,6 +6,7 @@ import { DraggableTreeContext } from "./DraggableTreeContext"; import DroppablePlaceholder from "./DroppablePlaceHolder"; import { DraggableTreeNode, DraggableTreeNodeItemRenderProps, IDragData, IDropData } from "./types"; import { checkDroppableFlag } from "./util"; +import { Flex } from "antd"; const DraggableMenuItemWrapper = styled.div` position: relative; @@ -88,29 +89,34 @@ export default function DraggableMenuItem(props: IDraggableMenuItemProps) { disabled={isDragging || disabled} /> )} - { - setDragNodeRef(node); - setDropNodeRef(node); - }} - {...dragListeners} - > - {renderContent?.({ - node: item, - isOver, - path, - isOverlay, - hasChildren: items.length > 0, - dragging: !!(isDragging || parentDragging), - isFolded: isFold, - onDelete: () => onDelete?.(path), - onToggleFold: () => context.toggleFold(id), - }) || null} - + + { + setDragNodeRef(node); + setDropNodeRef(node); + }} + {...dragListeners} + > + + +
+ {renderContent?.({ + node: item, + isOver, + path, + isOverlay, + hasChildren: items.length > 0, + dragging: !!(isDragging || parentDragging), + isFolded: isFold, + onDelete: () => onDelete?.(path), + onToggleFold: () => context.toggleFold(id), + }) || null} +
+
{items.length > 0 && !isFold && (
diff --git a/client/packages/lowcoder/src/components/NpmRegistryConfig.tsx b/client/packages/lowcoder/src/components/NpmRegistryConfig.tsx index 6dadfed36c..41e035bfda 100644 --- a/client/packages/lowcoder/src/components/NpmRegistryConfig.tsx +++ b/client/packages/lowcoder/src/components/NpmRegistryConfig.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { HelpText } from "./HelpText"; import { FormInputItem, FormSelectItem, TacoSwitch } from "lowcoder-design"; import { Form } from "antd"; -import { trans } from "@lowcoder-ee/i18n"; +import { trans } from "i18n"; import { FormStyled } from "@lowcoder-ee/pages/setting/idSource/styledComponents"; import { SaveButton } from "@lowcoder-ee/pages/setting/styled"; import { NpmRegistryConfigEntry } from "@lowcoder-ee/redux/reducers/uiReducers/commonSettingsReducer"; diff --git a/client/packages/lowcoder/src/components/TypographyText.tsx b/client/packages/lowcoder/src/components/TypographyText.tsx index 7bf1568593..81db5a69bb 100644 --- a/client/packages/lowcoder/src/components/TypographyText.tsx +++ b/client/packages/lowcoder/src/components/TypographyText.tsx @@ -40,9 +40,9 @@ const StyledTypographyText = styled(AntdTypographyText)` `; export const TypographyText = (props: { - value: string; - editing: boolean; - onChange: (value: string) => void; + value?: string; + editing?: boolean; + onChange?: (value: string) => void; }) => ( { if (typeof value === "number") { diff --git a/client/packages/lowcoder/src/comps/comps/imageComp.tsx b/client/packages/lowcoder/src/comps/comps/imageComp.tsx index aa0b4cc883..9e777fede5 100644 --- a/client/packages/lowcoder/src/comps/comps/imageComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/imageComp.tsx @@ -151,7 +151,7 @@ const ContainerImg = (props: RecordConstructorToView) => { src={props.src.value} referrerPolicy="same-origin" draggable={false} - preview={props.supportPreview} + preview={props.supportPreview ? {src: props.previewSrc || props.src.value } : false} fallback={DEFAULT_IMG_URL} onClick={() => props.onEvent("click")} /> @@ -170,6 +170,7 @@ const childrenMap = { animationStyle: styleControl(AnimationStyle , 'animationStyle'), autoHeight: withDefault(AutoHeightControl, "fixed"), supportPreview: BoolControl, + previewSrc: StringControl, restrictPaddingOnRotation:withDefault(StringControl, 'image') }; @@ -193,6 +194,9 @@ let ImageBasicComp = new UICompBuilder(childrenMap, (props) => { label: trans("image.supportPreview"), tooltip: trans("image.supportPreviewTip"), })} + {children.supportPreview.getView() && children.previewSrc.propertyView({ + label: trans("image.previewSrc") + })} )} diff --git a/client/packages/lowcoder/src/comps/comps/mediaComp/colorPickerConstants.tsx b/client/packages/lowcoder/src/comps/comps/mediaComp/colorPickerConstants.tsx index cb242546e9..c240d23667 100644 --- a/client/packages/lowcoder/src/comps/comps/mediaComp/colorPickerConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/mediaComp/colorPickerConstants.tsx @@ -1,4 +1,4 @@ -import { trans } from "@lowcoder-ee/i18n"; +import { trans } from "i18n"; export const presets = { diff --git a/client/packages/lowcoder/src/comps/comps/queryLibrary/queryLibraryComp.tsx b/client/packages/lowcoder/src/comps/comps/queryLibrary/queryLibraryComp.tsx index 7af0db9373..392ffbcc52 100644 --- a/client/packages/lowcoder/src/comps/comps/queryLibrary/queryLibraryComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/queryLibrary/queryLibraryComp.tsx @@ -47,9 +47,9 @@ const children = { const QueryLibraryCompBase = simpleMultiComp(children); export const QueryLibraryComp = class extends QueryLibraryCompBase { - propertyView(params: { onPublish: () => void; onHistoryShow: () => void }) { + propertyView(params: { onPublish: () => void; onHistoryShow: () => void; setModify: any; modify: boolean }) { return ( - + ); } @@ -99,11 +99,13 @@ function getMetaData( } const PropertyView = (props: { - comp: QueryLibraryCompType; - onPublish: () => void; - onHistoryShow: () => void; + comp: QueryLibraryCompType, + onPublish: () => void, + onHistoryShow: () => void, + setModify?: any + modify?: boolean }) => { - const { comp, onPublish, onHistoryShow } = props; + const { comp, onPublish, onHistoryShow, setModify, modify } = props; const reduxDispatch = useDispatch(); @@ -157,12 +159,16 @@ const PropertyView = (props: { CustomModal.confirm({ title: trans("queryLibrary.deleteQueryLabel"), content: trans("queryLibrary.deleteQueryContent"), - onConfirm: () => + onConfirm: () =>{ reduxDispatch( deleteQueryLibrary({ queryLibraryId: comp.children.query.children.id.getView(), }) - ), + ) + setTimeout(() => { + setModify(!modify); + }, 500); + }, confirmBtnType: "delete", okText: trans("delete"), }) diff --git a/client/packages/lowcoder/src/comps/comps/rootComp.tsx b/client/packages/lowcoder/src/comps/comps/rootComp.tsx index 5fede0b077..b28e4c0450 100644 --- a/client/packages/lowcoder/src/comps/comps/rootComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/rootComp.tsx @@ -1,5 +1,5 @@ -import "comps/comps/layout/navLayout"; -import "comps/comps/layout/mobileTabLayout"; +// import "comps/comps/layout/navLayout"; +// import "comps/comps/layout/mobileTabLayout"; import { CompAction, CompActionTypes } from "lowcoder-core"; import { EditorContext, EditorState } from "comps/editorState"; @@ -34,7 +34,7 @@ import { ExternalEditorContext } from "util/context/ExternalEditorContext"; import { useUserViewMode } from "util/hooks"; import React from "react"; import { isEqual } from "lodash"; - +import {LoadingBarHideTrigger} from "@lowcoder-ee/util/hideLoading"; const EditorView = lazy( () => import("pages/editor/editorView"), ); @@ -138,6 +138,7 @@ const RootView = React.memo((props: RootViewProps) => {
{comp.children.queries.children[key].getView()}
))} + diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx index 9278ec3262..f8c916404f 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx @@ -193,7 +193,7 @@ let StepControlBasicComp = (function () { > {props.options.map((option, index) => ( { + return +
123
+} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/CreateDropdown.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/CreateDropdown.tsx index c2d93086d3..787d3a2439 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/CreateDropdown.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/CreateDropdown.tsx @@ -185,14 +185,14 @@ function NavLayoutPickModal(props: { ); } -export const CreateDropdown = (props: { defaultVisible?: boolean; mode: HomeLayoutMode }) => { - const { defaultVisible, mode } = props; +export const CreateDropdown = (props: { defaultVisible?: boolean; mode: HomeLayoutMode; setModify: any; modify: boolean }) => { + const { defaultVisible, mode, setModify, modify} = props; const [createDropdownVisible, setCreateDropdownVisible] = useState(false); const [layoutPickerVisible, setLayoutPickerVisible] = useState(false); const user = useSelector(getUser); - const [handleCreate, isCreating] = useCreateHomeRes(); + const [handleCreate, isCreating] = useCreateHomeRes(setModify, modify); const getCreateMenuItem = (type: HomeResTypeEnum, mode?: HomeLayoutMode): ItemType => { if ( diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/FolderView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/FolderView.tsx index 1862533d87..695b932a98 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/FolderView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/FolderView.tsx @@ -1,13 +1,14 @@ import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; import { HomeBreadcrumbType, HomeLayout } from "./HomeLayout"; -import { useEffect } from "react"; -import { fetchFolderElements } from "../../redux/reduxActions/folderActions"; -import { FolderMeta } from "../../constants/applicationConstants"; +import {useEffect, useState} from "react"; +import {ApplicationCategoriesEnum, ApplicationMeta, FolderMeta} from "../../constants/applicationConstants"; import { buildFolderUrl } from "../../constants/routesURL"; import { folderElementsSelector, foldersSelector } from "../../redux/selectors/folderSelector"; import { Helmet } from "react-helmet"; import { trans } from "i18n"; +import {ApplicationPaginationType} from "@lowcoder-ee/util/pagination/type"; +import {fetchFolderElements} from "@lowcoder-ee/util/pagination/axios"; function getBreadcrumbs( folder: FolderMeta, @@ -30,12 +31,26 @@ function getBreadcrumbs( return breadcrumb; } +interface ElementsState { + elements: ApplicationMeta[]; + total: number; +} + export function FolderView() { const { folderId } = useParams<{ folderId: string }>(); + const [elements, setElements] = useState({ elements: [], total: 0 }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [searchValues, setSearchValues] = useState(""); + const [typeFilter, setTypeFilter] = useState(0); + const [modify, setModify] = useState(true); + const [searchValue, setSearchValue] = useState(""); + const [categoryFilter, setCategoryFilter] = useState("All"); + const dispatch = useDispatch(); - const elements = useSelector(folderElementsSelector); + const element = useSelector(folderElementsSelector); const allFolders = useSelector(foldersSelector); const folder = allFolders.filter((f) => f.folderId === folderId)[0] || {}; @@ -46,16 +61,62 @@ export function FolderView() { }, ]); - useEffect(() => { - setTimeout(() => { - dispatch(fetchFolderElements({ folderId: folderId })); - }, 100); - }, [folderId]); + useEffect( () => { + try{ + fetchFolderElements({ + id: folderId, + pageNum:currentPage, + pageSize:pageSize, + applicationType: ApplicationPaginationType[typeFilter], + name: searchValues, + category: categoryFilter === "All" ? "" : categoryFilter + }).then( + (data: any) => { + if (data.success) { + setElements({elements: data.data || [], total: data.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", data.error) + } + ); + } catch (error) { + console.error('Failed to fetch data:', error); + } + }, [currentPage, pageSize, searchValues, typeFilter, modify, categoryFilter]); + + useEffect( () => { + if (searchValues !== "") + setCurrentPage(1); + }, [searchValues] + ); + + useEffect(()=> { + const timer = setTimeout(() => { + if (searchValue.length > 2 || searchValue === "") + setSearchValues(searchValue) + }, 500); + return () => clearTimeout(timer); + }, [searchValue]) return ( <> {{trans("home.yourFolders")}} - + ); } diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeCardView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeCardView.tsx index ac515b5744..0ad356fbfc 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeCardView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeCardView.tsx @@ -1,7 +1,7 @@ import styled from "styled-components"; import { HomeRes } from "./HomeLayout"; -import { HomeResCard } from "./HomeResCard"; -import { MarketplaceResCard } from "./MarketplaceResCard"; +import {Back, HomeResCard} from "./HomeResCard"; +import { MarketplaceResCard} from "./MarketplaceResCard"; import React, { useState } from "react"; import { MoveToFolderModal } from "./MoveToFolderModal"; @@ -19,17 +19,19 @@ const ApplicationCardsWrapper = styled.div` } `; -export function HomeCardView(props: { resources: HomeRes[] }) { +export function HomeCardView(props: { resources: HomeRes[], setModify?: any, modify?: boolean, mode?: string }) { + const {setModify, modify,mode} = props; const [needMoveRes, setNeedMoveRes] = useState(undefined); return ( + {props.resources.map((res) => ( res.isMarketplace ? : - + ))} - setNeedMoveRes(undefined)} /> + setNeedMoveRes(undefined)} setModify={setModify} modify={modify!} /> ); } diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx index e69792bbd5..6005ed071a 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx @@ -35,6 +35,7 @@ import { isFetchingFolderElements } from "../../redux/selectors/folderSelector"; import { checkIsMobile } from "util/commonUtils"; import { default as Divider } from "antd/es/divider"; import { ApplicationCategoriesEnum } from "constants/applicationConstants"; +import { Pagination } from 'antd'; const Wrapper = styled.div` display: flex; @@ -199,6 +200,12 @@ const EmptyView = styled.div` } } `; +const PaginationLayout = styled.div` + display: flex; + justify-content: center; + margin-top: 20px; + margin-bottom: 20px; +` const LayoutSwitcher = styled.div` position: absolute; @@ -301,11 +308,51 @@ export interface HomeLayoutProps { localMarketplaceApps?: Array; globalMarketplaceApps?: Array; mode: HomeLayoutMode; + setCurrentPage?: any; + setPageSize?: any; + currentPage?: number; + pageSize?: number; + total?: number; + searchValue?: string; + setSearchValue?: any; + setTypeFilterPagination?: any; + setCategoryFilterPagination?: any; + setIsCreated?: any; + isCreated?: boolean; + setModify?: any; + modify?: boolean; } export function HomeLayout(props: HomeLayoutProps) { + const { breadcrumb = [], + elements = [], + localMarketplaceApps = [], + globalMarketplaceApps = [], + mode , + setCurrentPage, + setPageSize, + pageSize, + currentPage, + searchValue, + setSearchValue, + total, + setTypeFilterPagination, + setCategoryFilterPagination, + setModify, + modify, + setIsCreated, + isCreated + + } = props; + + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; - const { breadcrumb = [], elements = [], localMarketplaceApps = [], globalMarketplaceApps = [], mode } = props; + const handlePageSizeChange = (current: number, size: number) => { + setPageSize(size); + }; const categoryOptions = [ { label: {trans("home.allCategories")}, value: 'All' }, @@ -324,7 +371,7 @@ export function HomeLayout(props: HomeLayoutProps) { const isSelfHost = window.location.host !== 'app.lowcoder.cloud'; const [typeFilter, setTypeFilter] = useState("All"); const [categoryFilter, setCategoryFilter] = useState("All"); - const [searchValue, setSearchValue] = useState(""); + const [visibility, setVisibility] = useState(mode === "view" || mode === "trash" || mode === "folder"); const [layout, setLayout] = useState( checkIsMobile(window.innerWidth) ? "card" : getHomeLayout() ); @@ -342,7 +389,15 @@ export function HomeLayout(props: HomeLayoutProps) { return null; } - var displayElements = elements; + var displayElements = elements.sort((a, b) => { + if (a.folder && !b.folder) { + return -1; + } else if (!a.folder && b.folder) { + return 1; + } else { + return 0; + } + }); if (mode === "marketplace" && isSelfHost) { const markedLocalApps = localMarketplaceApps.map(app => ({ ...app, isLocalMarketplace: true })); @@ -354,36 +409,34 @@ export function HomeLayout(props: HomeLayoutProps) { const markedLocalApps = localMarketplaceApps.map(app => ({ ...app, isLocalMarketplace: true })); displayElements = [...markedLocalApps]; } - const resList: HomeRes[] = displayElements - .filter((e) => - searchValue - ? e.name?.toLocaleLowerCase().includes(searchValue?.toLocaleLowerCase()) || - e.createBy?.toLocaleLowerCase().includes(searchValue?.toLocaleLowerCase()) - : true - ) .filter((e) => { - if (HomeResTypeEnum[typeFilter].valueOf() === HomeResTypeEnum.All) { - return true; - } - if (e.folder) { - return HomeResTypeEnum[typeFilter] === HomeResTypeEnum.Folder; - } else { - if (typeFilter === "Navigation") { - return NavigationTypes.map((t) => t.valueOf()).includes(e.applicationType); + if (!visibility) { + if (searchValue) { + const lowerCaseSearchValue = searchValue.toLocaleLowerCase(); + return e.name?.toLocaleLowerCase().includes(lowerCaseSearchValue) || + e.createBy?.toLocaleLowerCase().includes(lowerCaseSearchValue); } - return HomeResTypeEnum[typeFilter].valueOf() === e.applicationType; + return true; } + return true; }) .filter((e) => { - // If "All" is selected, do not filter out any elements based on category - if (categoryFilter === 'All' || !categoryFilter) { - return true; + if(!visibility) { + if (HomeResTypeEnum[typeFilter].valueOf() === HomeResTypeEnum.All) { + return true; + } + if (e.folder) { + return HomeResTypeEnum[typeFilter] === HomeResTypeEnum.Folder; + } else { + if (typeFilter === "Navigation") { + return NavigationTypes.map((t) => t.valueOf()).includes(e.applicationType); + } + return HomeResTypeEnum[typeFilter].valueOf() === e.applicationType; + } } - // Otherwise, filter elements based on the selected category - return !e.folder && e.category === categoryFilter.toString(); - }) - + return true; + }) .map((e) => e.folder ? { @@ -462,7 +515,7 @@ export function HomeLayout(props: HomeLayoutProps) { {showNewUserGuide(user) && } - +

{mode === "marketplace" && trans("home.appMarketplace")} @@ -480,17 +533,37 @@ export function HomeLayout(props: HomeLayoutProps) { setTypeFilter(value as HomeResKey)} + onChange={(value: any) => { + setTypeFilter(value as HomeResKey); + if(visibility) + setTypeFilterPagination(HomeResTypeEnum[value]) + } + } options={[ getFilterMenuItem(HomeResTypeEnum.All), getFilterMenuItem(HomeResTypeEnum.Application), getFilterMenuItem(HomeResTypeEnum.Module), - ...(mode !== "marketplace" ? [getFilterMenuItem(HomeResTypeEnum.Navigation)] : []), - ...(mode !== "trash" && mode !== "marketplace" ? [getFilterMenuItem(HomeResTypeEnum.Folder)] : []), + ...(mode !== "marketplace" ? [getFilterMenuItem(HomeResTypeEnum.Navigation), getFilterMenuItem(HomeResTypeEnum.MobileTabLayout)] : []), + ...(mode !== "trash" && mode !== "marketplace" && mode !== "folder" ? [getFilterMenuItem(HomeResTypeEnum.Folder)] : []), ]} getPopupContainer={(node: any) => node} suffixIcon={} /> )} + {(mode === "view" || mode === "folder") && + { + setCategoryFilter(value as ApplicationCategoriesEnum) + setCategoryFilterPagination(value as ApplicationCategoriesEnum); + } + + } + options={categoryOptions} + // getPopupContainer={(node) => node} + suffixIcon={} + />} {mode === "marketplace" && ( setSearchValue(e.target.value)} style={{ width: "192px", height: "32px", margin: "0" }} /> {mode !== "trash" && mode !== "marketplace" && user.orgDev && ( - + )} @@ -526,7 +599,7 @@ export function HomeLayout(props: HomeLayoutProps) { {resList.length > 0 ? ( <> {mode === "trash" ? ( - + ) : ( <> setLayout(layout === "list" ? "card" : "list")}> @@ -575,9 +648,9 @@ export function HomeLayout(props: HomeLayoutProps) { {mode !== "marketplace" && ( <> {layout === "list" ? ( - + ) : ( - + )} )} @@ -597,16 +670,27 @@ export function HomeLayout(props: HomeLayoutProps) { ? trans("home.projectEmptyCanAdd") : trans("home.projectEmpty")}

- {mode !== "trash" && mode !== "marketplace" && user.orgDev && } + {mode !== "trash" && mode !== "marketplace" && user.orgDev && } )} )} - + {visibility && resList.length ?
+ + + +
: null} - + ); diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeResCard.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeResCard.tsx index 846d59cbf4..0ce7840471 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeResCard.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeResCard.tsx @@ -8,6 +8,7 @@ import { HomeRes } from "./HomeLayout"; import { HomeResTypeEnum } from "../../types/homeRes"; import { updateFolder } from "../../redux/reduxActions/folderActions"; import { + backFolderViewClick, handleAppEditClick, handleAppViewClick, handleFolderViewClick, @@ -23,6 +24,7 @@ import { TypographyText } from "../../components/TypographyText"; import { useParams } from "react-router-dom"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { colorPickerEvent } from "@lowcoder-ee/comps/comps/mediaComp/colorPickerComp"; +import {FolderIcon} from "icons"; const EditButton = styled(TacoButton)` width: 52px; @@ -141,8 +143,8 @@ const OperationWrapper = styled.div` const MONTH_MILLIS = 30 * 24 * 60 * 60 * 1000; -export function HomeResCard(props: { res: HomeRes; onMove: (res: HomeRes) => void }) { - const { res, onMove } = props; +export function HomeResCard(props: { res: HomeRes; onMove: (res: HomeRes) => void; setModify:any; modify: boolean }) { + const { res, onMove, setModify, modify } = props; const [appNameEditing, setAppNameEditing] = useState(false); const dispatch = useDispatch(); @@ -214,10 +216,16 @@ export function HomeResCard(props: { res: HomeRes; onMove: (res: HomeRes) => voi } if (res.type === HomeResTypeEnum.Folder) { dispatch(updateFolder({ id: res.id, name: value })); + setTimeout(() => { + setModify(!modify); + }, 200); } else { dispatch( updateAppMetaAction({ applicationId: res.id, name: value, folderId: folderId }) ); + setTimeout(() => { + setModify(!modify); + }, 200); } setAppNameEditing(false); }} @@ -245,9 +253,37 @@ export function HomeResCard(props: { res: HomeRes; onMove: (res: HomeRes) => voi res={res} onRename={() => setAppNameEditing(true)} onMove={(res) => onMove(res)} + setModify={setModify} + modify={modify} /> ); } + +export function Back(props: { mode: string }) { + const { mode } = props; + return mode === "folder" ? + + + + { + backFolderViewClick(); + }} + > + +

...

+ +
+
+
+ : <>; +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeResOptions.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeResOptions.tsx index b712fe7e40..0049ff1b6e 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeResOptions.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeResOptions.tsx @@ -38,8 +38,10 @@ export const HomeResOptions = (props: { onDuplicate?: (res: HomeRes | undefined) => void; onRename: (res: HomeRes) => void; onMove: (res: HomeRes) => void; + setModify: any; + modify: boolean; }) => { - const { res, onDuplicate, onRename, onMove } = props; + const { res, onDuplicate, onRename, onMove, setModify, modify } = props; const dispatch = useDispatch(); const [showCopyModal, setShowCopyModal] = useState(false); @@ -78,19 +80,24 @@ export const HomeResOptions = (props: { type: HomeResInfo[res.type].name, name: {res.name}, }), - onConfirm: () => + onConfirm: () =>{ new Promise((resolve, reject) => { dispatch( - recycleApplication( - { applicationId: res.id, folderId: folderId }, - () => { - messageInstance.success(trans("success")); - resolve(true); - }, - () => reject() - ) + recycleApplication( + { applicationId: res.id, folderId: folderId }, + () => { + messageInstance.success(trans("success")); + resolve(true); + }, + () => reject() + ) ); - }), + setTimeout(() => { + setModify(!modify); + }, 200); + }) + + }, confirmBtnType: "delete", okText: trans("home.moveToTrash"), }); @@ -115,19 +122,23 @@ export const HomeResOptions = (props: { type: HomeResInfo[res.type].name.toLowerCase(), name: {res.name}, }), - onConfirm: () => + onConfirm: () =>{ new Promise((resolve, reject) => { - dispatch( + dispatch( deleteFolder( - { folderId: res.id, parentFolderId: folderId }, - () => { - messageInstance.success(trans("home.deleteSuccessMsg")); - resolve(true); - }, - () => reject() + { folderId: res.id, parentFolderId: folderId }, + () => { + messageInstance.success(trans("home.deleteSuccessMsg")); + resolve(true); + }, + () => reject() ) - ); - }), + ); + }) + setTimeout(() => { + setModify(!modify); + }, 200); + }, confirmBtnType: "delete", okText: trans("delete"), }); diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeTableView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeTableView.tsx index 1eeb261e6c..bd0cf6b82c 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeTableView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeTableView.tsx @@ -4,6 +4,7 @@ import { TacoButton } from "lowcoder-design/src/components/button" import styled from "styled-components"; import { useDispatch } from "react-redux"; import { + backFolderViewClick, handleAppEditClick, handleAppViewClick, handleFolderViewClick, @@ -51,7 +52,8 @@ const TypographyText = styled(AntdTypographyText)` width: 100%; `; -export const HomeTableView = (props: { resources: HomeRes[] }) => { +export const HomeTableView = (props: { resources: HomeRes[], setModify?: any, modify?: boolean, mode?: string }) => { + const {setModify, modify, resources, mode} = props const dispatch = useDispatch(); const { folderId } = useParams<{ folderId: string }>(); @@ -60,26 +62,43 @@ export const HomeTableView = (props: { resources: HomeRes[] }) => { const [needDuplicateRes, setNeedDuplicateRes] = useState(undefined); const [needMoveRes, setNeedMoveRes] = useState(undefined); + const back: HomeRes = { + key: "", + id: "", + name: ". . .", + type: 4, + creator: "", + lastModifyTime: 0, + isManageable: false, + isDeletable: false + } + if (mode === "folder"){ + resources.unshift(back) + } + return ( <> ({ onClick: (e) => { - // console.log(e.target); - const item = record as HomeRes; - if (needRenameRes?.id === item.id || needDuplicateRes?.id === item.id) { - return; - } - if (item.type === HomeResTypeEnum.Folder) { - handleFolderViewClick(item.id); - } else if(item.isMarketplace) { - handleMarketplaceAppViewClick(item.id); - } else { - item.isEditable ? handleAppEditClick(e, item.id) : handleAppViewClick(item.id); + if (mode === "folder" && record.type === 4){ + backFolderViewClick() + } else{ + const item = record as HomeRes; + if (needRenameRes?.id === item.id || needDuplicateRes?.id === item.id) { + return; + } + if (item.type === HomeResTypeEnum.Folder) { + handleFolderViewClick(item.id); + } else if(item.isMarketplace) { + handleMarketplaceAppViewClick(item.id); + } else { + item.isEditable ? handleAppEditClick(e, item.id) : handleAppViewClick(item.id); + } } }, })} @@ -122,6 +141,9 @@ export const HomeTableView = (props: { resources: HomeRes[] }) => { } if (item.type === HomeResTypeEnum.Folder) { dispatch(updateFolder({ id: item.id, name: value })); + setTimeout(() => { + setModify(!modify); + }, 200); } else { dispatch( updateAppMetaAction({ @@ -130,6 +152,9 @@ export const HomeTableView = (props: { resources: HomeRes[] }) => { folderId: folderId, }) ); + setTimeout(() => { + setModify(!modify); + }, 200); } setNeedRenameRes(undefined); }, @@ -154,7 +179,7 @@ export const HomeTableView = (props: { resources: HomeRes[] }) => { }, render: (_, record) => ( - {HomeResInfo[(record as any).type as HomeResTypeEnum].name} + { mode === "folder" && record.type === 4 ? "" : HomeResInfo[(record as any).type as HomeResTypeEnum].name } ), }, @@ -216,7 +241,7 @@ export const HomeTableView = (props: { resources: HomeRes[] }) => { ? handleMarketplaceAppViewClick(item.id) : handleAppViewClick(item.id); }} - style={{ marginRight: "52px" }} + style={{ marginRight: "52px", display: mode === "folder" && record.type === 4 ? "none" : "block" }} > {trans("view")} @@ -225,15 +250,17 @@ export const HomeTableView = (props: { resources: HomeRes[] }) => { onDuplicate={(res) => setNeedDuplicateRes(res)} onRename={(res) => setNeedRenameRes(res)} onMove={(res) => setNeedMoveRes(res)} + setModify={setModify} + modify={modify!} /> ); }, }, ]} - dataSource={props.resources} + dataSource={resources} /> - setNeedMoveRes(undefined)} /> + setNeedMoveRes(undefined)} setModify={setModify} modify={modify!} /> ); }; diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeView.tsx index b4309e321e..3a435a6b87 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeView.tsx @@ -1,12 +1,66 @@ import { useSelector } from "react-redux"; import { HomeLayout } from "./HomeLayout"; import { getUser } from "../../redux/selectors/usersSelectors"; -import { folderElementsSelector } from "../../redux/selectors/folderSelector"; import { Helmet } from "react-helmet"; import { trans } from "i18n"; +import {useState, useEffect } from "react"; +import {fetchFolderElements} from "@lowcoder-ee/util/pagination/axios"; +import {ApplicationCategoriesEnum, ApplicationMeta, FolderMeta} from "@lowcoder-ee/constants/applicationConstants"; +import {ApplicationPaginationType} from "@lowcoder-ee/util/pagination/type"; + +interface ElementsState { + elements: (ApplicationMeta | FolderMeta)[]; + total: number; +} export function HomeView() { - const elements = useSelector(folderElementsSelector)[""]; + const [elements, setElements] = useState({ elements: [], total: 1 }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [searchValue, setSearchValue] = useState(""); + const [searchValues, setSearchValues] = useState(""); + const [typeFilter, setTypeFilter] = useState(0); + const [modify, setModify] = useState(true); + const [isCreated, setIsCreated] = useState(true); + const [categoryFilter, setCategoryFilter] = useState("All"); + + useEffect( () => { + try{ + fetchFolderElements({ + pageNum:currentPage, + pageSize:pageSize, + applicationType: ApplicationPaginationType[typeFilter], + name: searchValues, + category: categoryFilter === "All" ? "" : categoryFilter + }).then( + (data: any) => { + if (data.success) { + setElements({elements: data.data || [], total: data.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", data.error) + } + ); + } catch (error) { + console.error('Failed to fetch data:', error); + } + }, [currentPage, pageSize, searchValues, typeFilter, modify, categoryFilter, isCreated] + ); + + useEffect( () => { + if (searchValues !== "") + setCurrentPage(1); + }, [searchValues] + ); + + useEffect(()=> { + const timer = setTimeout(() => { + if (searchValue.length > 2 || searchValue === "") + setSearchValues(searchValue) + }, 500); + return () => clearTimeout(timer); + }, [searchValue]) + const user = useSelector(getUser); if (!user.currentOrgId) { @@ -16,9 +70,22 @@ export function HomeView() { return ( <> {{trans("productName")} {trans("home.home")}} - ); diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceView.tsx index 01b76fb787..185c2b18b9 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceView.tsx @@ -13,6 +13,7 @@ import { Helmet } from "react-helmet"; export function MarketplaceView() { const [ marketplaceApps, setMarketplaceApps ] = useState>([]); const [ localMarketplaceApps, setLocalMarketplaceApps ] = useState>([]); + const [searchValue, setSearchValue] = useState(""); const fetchMarketplaceApps = async () => { try { @@ -60,7 +61,10 @@ export function MarketplaceView() { localMarketplaceApps={localMarketplaceApps} globalMarketplaceApps={marketplaceApps} breadcrumb={[{ text: trans("home.marketplace"), path: MARKETPLACE_URL }]} - mode={"marketplace"} /> + mode={"marketplace"} + searchValue={searchValue} + setSearchValue={setSearchValue} + /> ); }; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/MoveToFolderModal.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/MoveToFolderModal.tsx index 5610209057..34bd6b9a1b 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/MoveToFolderModal.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/MoveToFolderModal.tsx @@ -40,7 +40,8 @@ const MoveModalFooter = styled.div` gap: 8px; `; -export const MoveToFolderModal = (props: { source?: HomeRes; onClose: () => void }) => { +export const MoveToFolderModal = (props: { source?: HomeRes; onClose: () => void, setModify: any, modify: boolean }) => { + const {setModify, modify} = props; const [form] = Form.useForm(); const [loading, setLoading] = useState(false); @@ -83,6 +84,9 @@ export const MoveToFolderModal = (props: { source?: HomeRes; onClose: () => void () => setLoading(false) ) ); + setTimeout(() => { + setModify(!modify); + }, 200); }); }} > diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/RootFolderListView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/RootFolderListView.tsx deleted file mode 100644 index a2263017cf..0000000000 --- a/client/packages/lowcoder/src/pages/ApplicationV2/RootFolderListView.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useSelector } from "react-redux"; -import { HomeLayout } from "./HomeLayout"; -import { getUser } from "../../redux/selectors/usersSelectors"; -import { FOLDERS_URL } from "../../constants/routesURL"; -import { trans } from "../../i18n"; -import { foldersSelector } from "../../redux/selectors/folderSelector"; - -export function RootFolderListView() { - const user = useSelector(getUser); - const allFolders = useSelector(foldersSelector); - - if (!user.currentOrgId) { - return null; - } - - return ( - - ); -} diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/TrashTableView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/TrashTableView.tsx index 0b600a4724..424d675073 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/TrashTableView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/TrashTableView.tsx @@ -32,7 +32,8 @@ const EditBtn = styled(TacoButton)` height: 24px; `; -export const TrashTableView = (props: { resources: HomeRes[] }) => { +export const TrashTableView = (props: { resources: HomeRes[] , setModify: any, modify: boolean }) => { + const {resources, setModify, modify} = props; const dispatch = useDispatch(); return ( @@ -119,13 +120,17 @@ export const TrashTableView = (props: { resources: HomeRes[] }) => { style={{ padding: "0 8px", width: "fit-content", minWidth: "52px" }} buttonType={"blue"} className={"home-datasource-edit-button"} - onClick={() => - dispatch( - restoreApplication({ applicationId: item.id }, () => { - messageInstance.success(trans("home.recoverSuccessMsg")); - }) - ) + onClick={() =>{ + dispatch( + restoreApplication({ applicationId: item.id }, () => { + messageInstance.success(trans("home.recoverSuccessMsg")); + }) + ) + setTimeout(() => { + setModify(!modify); + }, 200); } + } > {trans("recover")} @@ -140,7 +145,7 @@ export const TrashTableView = (props: { resources: HomeRes[] }) => { type: HomeResInfo[item.type].name.toLowerCase(), name: {item.name}, }), - onConfirm: () => + onConfirm: () =>{ new Promise((resolve, reject) => { dispatch( deleteApplication( @@ -152,10 +157,15 @@ export const TrashTableView = (props: { resources: HomeRes[] }) => { () => reject() ) ); - }), + }) + setTimeout(() => { + setModify(!modify); + }, 200); + }, confirmBtnType: "delete", okText: trans("delete"), }) + } style={{ marginLeft: "12px", width: "76px" }} > @@ -166,7 +176,7 @@ export const TrashTableView = (props: { resources: HomeRes[] }) => { }, }, ]} - dataSource={props.resources} + dataSource={resources} /> ); }; diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/TrashView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/TrashView.tsx index d1b0586c27..410a2632f0 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/TrashView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/TrashView.tsx @@ -1,27 +1,79 @@ import { HomeLayout } from "./HomeLayout"; -import { useDispatch, useSelector } from "react-redux"; -import { recycleListSelector } from "../../redux/selectors/applicationSelector"; import { TRASH_URL } from "../../constants/routesURL"; -import { useEffect } from "react"; -import { fetchApplicationRecycleList } from "../../redux/reduxActions/applicationActions"; +import {useEffect, useState} from "react"; import { trans } from "../../i18n"; import { Helmet } from "react-helmet"; +import {fetchApplicationElements} from "@lowcoder-ee/util/pagination/axios"; + +interface ElementsState { + elements: any; + total: number; +} export function TrashView() { - const dispatch = useDispatch(); - const recycleList = useSelector(recycleListSelector); + const [elements, setElements] = useState({ elements: [], total: 1 }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [searchValues, setSearchValues] = useState(""); + const [searchValue, setSearchValue] = useState(""); + const [typeFilter, setTypeFilter] = useState(0); + const [modify, setModify] = useState(false); - useEffect(() => { - dispatch(fetchApplicationRecycleList()); - }, [dispatch]); + useEffect( () => { + try{ + fetchApplicationElements({ + pageNum:currentPage, + pageSize:pageSize, + applicationType: typeFilter === 7 ? 3 : typeFilter, // // Application of Navigation is 3 in API. + name: searchValues, + }).then( + data => { + if (data.success) { + setElements({elements: data.data || [], total: data.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", data.error) + } + ); + } catch (error) { + console.error('Failed to fetch data:', error); + } + }, [currentPage, pageSize, searchValues, typeFilter, modify] + ); + useEffect( () => { + if (searchValues !== "") + setCurrentPage(1); + }, [searchValues] + ); + + //debouncing + useEffect(()=> { + const timer = setTimeout(() => { + if (searchValue.length > 2 || searchValue === "") + setSearchValues(searchValue) + }, 500); + return () => clearTimeout(timer); + }, [searchValue]) return ( <> {{trans("home.trash")}} + elements={elements.elements} + breadcrumb={[{ text: trans("home.trash"), path: TRASH_URL }]} + mode={"trash"} + currentPage ={currentPage} + setCurrentPage={setCurrentPage} + pageSize={pageSize} + setPageSize={setPageSize} + total={elements.total} + setSearchValue={setSearchValue} + searchValue={searchValue} + setTypeFilterPagination={setTypeFilter} + setModify={setModify} + modify={modify} + /> ); } + diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx index c6fd5f91fb..5a3a2f3fac 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx @@ -4,7 +4,6 @@ import { DATASOURCE_URL, FOLDER_URL, FOLDER_URL_PREFIX, - FOLDERS_URL, MARKETPLACE_URL, QUERY_LIBRARY_URL, SETTING_URL, @@ -53,7 +52,6 @@ import { FolderView } from "./FolderView"; import { TrashView } from "./TrashView"; import { MarketplaceView } from "./MarketplaceView"; // import { SideBarItemType } from "../../components/layout/SideBarSection"; -import { RootFolderListView } from "./RootFolderListView"; // import InviteDialog from "../common/inviteDialog"; import { fetchFolderElements, updateFolder } from "../../redux/reduxActions/folderActions"; // import { ModuleView } from "./ModuleView"; @@ -73,7 +71,7 @@ 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` font-weight: 500; `; @@ -222,6 +220,7 @@ export default function ApplicationHome() { return ( + {trans("home.allFolders")}, - routePath: FOLDERS_URL, - routeComp: RootFolderListView, - icon: ({ selected, ...otherProps }) => selected ? : , - }, + // { + // text: {trans("home.allFolders")}, + // routePath: FOLDERS_URL, + // routeComp: RootFolderListView, + // icon: ({ selected, ...otherProps }) => selected ? : , + // }, { text: {trans("home.allApplications")}, routePath: ALL_APPLICATIONS_URL, diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/useCreateFolder.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/useCreateFolder.tsx index 4c12439497..04c50f22c7 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/useCreateFolder.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/useCreateFolder.tsx @@ -17,7 +17,7 @@ const CreateFolderLabel = styled.div` margin-bottom: 8px; `; -export function useCreateFolder() { +export function useCreateFolder(setModify: any, modify: boolean) { const dispatch = useDispatch(); const user = useSelector(getUser); const allFolders = useSelector(foldersSelector); @@ -73,7 +73,7 @@ export function useCreateFolder() { ), - onConfirm: () => + onConfirm: () =>{ form.validateFields().then( () => new Promise((resolve, reject) => { @@ -82,7 +82,11 @@ export function useCreateFolder() { () => reject(false) ); }) - ), + ) + setTimeout(() => { + setModify(!modify); + }, 200); + }, okText: trans("create"), }); }, [user, allFolders, form, dispatch]); diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/useCreateHomeRes.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/useCreateHomeRes.tsx index 6198279b85..7c314ab11f 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/useCreateHomeRes.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/useCreateHomeRes.tsx @@ -31,7 +31,7 @@ export const newAppPrefix = (userName: string, appType: AppTypeEnum = AppTypeEnu return trans("home.newApp", { userName: userName, name: toLower(HomeResInfo[appType].name) }); }; -export function useCreateHomeRes() { +export function useCreateHomeRes(setModify:any, modify: boolean) { const dispatch = useDispatch(); const user = useSelector(getUser); const allApplications = useSelector(normalAppListSelector); @@ -39,7 +39,7 @@ export function useCreateHomeRes() { const { folderId } = useParams<{ folderId: string }>(); - const handleFolderCreate = useCreateFolder(); + const handleFolderCreate = useCreateFolder(setModify, modify); const handleCreate = useCallback( (type: HomeResTypeEnum) => { diff --git a/client/packages/lowcoder/src/pages/common/header.tsx b/client/packages/lowcoder/src/pages/common/header.tsx index 21844023e2..60b02b6f03 100644 --- a/client/packages/lowcoder/src/pages/common/header.tsx +++ b/client/packages/lowcoder/src/pages/common/header.tsx @@ -372,7 +372,7 @@ export default function Header(props: HeaderProps) { const applicationId = useApplicationId(); const dispatch = useDispatch(); const showAppSnapshot = useSelector(showAppSnapshotSelector); - const selectedSnapshot = useSelector(getSelectedAppSnapshot); + const {selectedSnapshot, isArchivedSnapshot} = useSelector(getSelectedAppSnapshot); const { appType } = useContext(ExternalEditorContext); const [editName, setEditName] = useState(false); const [editing, setEditing] = useState(false); @@ -512,7 +512,8 @@ export default function Header(props: HeaderProps) { recoverSnapshotAction( application.applicationId, selectedSnapshot.snapshotId, - selectedSnapshot.createTime + selectedSnapshot.createTime, + isArchivedSnapshot, ) ); }, diff --git a/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx b/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx index 87fb7ec08e..f85ab88baf 100644 --- a/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx +++ b/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx @@ -1,6 +1,6 @@ import styled from "styled-components"; import { EditPopover, PointIcon, Search, TacoButton } from "lowcoder-design"; -import React, { useState } from "react"; +import React, {useEffect, useState} from "react"; import { useDispatch, useSelector } from "react-redux"; import { getDataSource, getDataSourceLoading, getDataSourceTypesMap } from "../../redux/selectors/datasourceSelectors"; import { deleteDatasource } from "../../redux/reduxActions/datasourceActions"; @@ -17,6 +17,10 @@ import { DatasourcePermissionDialog } from "../../components/PermissionDialog/Da import DataSourceIcon from "components/DataSourceIcon"; import { Helmet } from "react-helmet"; import LoadingOutlined from "@ant-design/icons/LoadingOutlined"; +import PaginationComp from "@lowcoder-ee/util/pagination/Pagination"; +import {DatasourceInfo} from "@lowcoder-ee/api/datasourceApi"; +import {fetchDatasourcePagination} from "@lowcoder-ee/util/pagination/axios"; +import {getUser} from "@lowcoder-ee/redux/selectors/usersSelectors"; const DatasourceWrapper = styled.div` display: flex; @@ -103,11 +107,54 @@ const StyledTable = styled(Table)` export const DatasourceList = () => { const dispatch = useDispatch(); const [searchValue, setSearchValue] = useState(""); + const [searchValues, setSearchValues] = useState(""); const [isCreateFormShow, showCreateForm] = useState(false); const [shareDatasourceId, setShareDatasourceId] = useState(undefined); - const datasource = useSelector(getDataSource); + const [modify, setModify] = useState(false); + const currentUser = useSelector(getUser); + const orgId = currentUser.currentOrgId; const datasourceLoading = useSelector(getDataSourceLoading); const plugins = useSelector(getDataSourceTypesMap); + interface ElementsState { + elements: DatasourceInfo[]; + total: number; + } + + const [elements, setElements] = useState({ elements: [], total: 0 }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + useEffect(()=> { + const timer = setTimeout(() => { + if (searchValue.length > 2 || searchValue === "") + setSearchValues(searchValue) + }, 500); + return () => clearTimeout(timer); + }, [searchValue]) + + useEffect( () => { + fetchDatasourcePagination( + { + orgId: orgId, + pageNum: currentPage, + pageSize: pageSize, + name: searchValues + } + ).then((result: any) => { + if (result.success){ + setElements({elements: result.data || [], total: result.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", result.error) + }) + }, [currentPage, pageSize, searchValues, modify] + ) + + useEffect( () => { + if (searchValues !== "") + setCurrentPage(1); + }, [searchValues] + ); return ( <> @@ -254,6 +301,10 @@ export const DatasourceList = () => { text: trans("delete"), onClick: () => { dispatch(deleteDatasource({ datasourceId: record.id })); + setTimeout(() => { + setModify(!modify); + }, 500); + }, type: "delete", }, @@ -267,19 +318,7 @@ export const DatasourceList = () => { ), }, ]} - dataSource={datasource - .filter((info) => { - if (info.datasource.creationSource === 2) { - return false; - } - if (!isEmpty(searchValue)) { - return ( - info.datasource.name.toLowerCase().includes(searchValue.trim().toLowerCase()) || - info.datasource.type.toLowerCase().includes(searchValue.trim().toLowerCase()) - ); - } - return true; - }) + dataSource={elements.elements .map((info, i) => ({ key: i, id: info.datasource.id, @@ -296,6 +335,13 @@ export const DatasourceList = () => { creator: info.creatorName, edit: info.edit, }))} /> + { !!elements.elements.length ? : <>} {shareDatasourceId && ( { !visible && setShareDatasourceId(undefined); } } /> )} - + + + ); }; diff --git a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx index 0af4823f1e..512e2d8d1d 100644 --- a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx +++ b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx @@ -37,6 +37,8 @@ import { currentApplication } from "@lowcoder-ee/redux/selectors/applicationSele import { notificationInstance } from "components/GlobalInstances"; import { AppState } from "@lowcoder-ee/redux/reducers"; import { resetIconDictionary } from "@lowcoder-ee/constants/iconConstants"; +import {fetchJsDSPaginationByApp} from "@lowcoder-ee/util/pagination/axios"; +import PaginationComp from "@lowcoder-ee/util/pagination/Pagination"; const AppSnapshot = lazy(() => { return import("pages/editor/appSnapshot") @@ -57,6 +59,9 @@ const AppEditor = React.memo(() => { 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( @@ -140,8 +145,13 @@ const AppEditor = React.memo(() => { }, [dispatch, applicationId, paramViewMode]); const fetchJSDataSourceByApp = useCallback(() => { - DatasourceApi.fetchJsDatasourceByApp(applicationId).then((res) => { - res.data.data.forEach((i) => { + 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); @@ -153,6 +163,8 @@ const AppEditor = React.memo(() => { setIsDataSourcePluginRegistered, setShowAppSnapshot, dispatch, + currentPage, + pageSize ]); useEffect(() => { @@ -219,6 +231,13 @@ const AppEditor = React.memo(() => { return ( + {/**/} {showAppSnapshot ? ( }> (currentAppInfo); const isSnapshotDslLoading = useSelector(isAppSnapshotDslFetching); const compInstance = useRootCompInstance(appInfo, true, true); + const [activeTab, setActiveTab] = useState("recent"); + + const isArchivedSnapshot = useMemo(() => activeTab === 'archive', [activeTab]); - const fetchSnapshotList = (page: number, onSuccess?: (snapshots: AppSnapshotList) => void) => { - dispatch(setSelectSnapshotId("")); + const fetchSnapshotList = useCallback((page: number, onSuccess?: (snapshots: AppSnapshotList) => void) => { + dispatch(setSelectSnapshotId("", isArchivedSnapshot)); application && dispatch( fetchSnapshotsAction({ applicationId: application.applicationId, page: page, size: PAGE_SIZE, + archived: isArchivedSnapshot, onSuccess: onSuccess, }) ); - }; + }, [application, activeTab]); - useMount(() => { + + useEffect(() => { if (!application) { return; } @@ -174,12 +183,17 @@ export const AppSnapshot = React.memo((props: { currentAppInfo: AppSummaryInfo } return; } dispatch( - fetchSnapshotDslAction(application.applicationId, snapshots.list[0].snapshotId, (res) => { - setLatestDsl(res); - }) + fetchSnapshotDslAction( + application.applicationId, + snapshots.list[0].snapshotId, + isArchivedSnapshot, + (res) => { + setLatestDsl(res); + } + ) ); }); - }); + }, [application, activeTab]); useEffect(() => { currentDsl && @@ -193,7 +207,10 @@ export const AppSnapshot = React.memo((props: { currentAppInfo: AppSummaryInfo } return; } setSelectedItemKey(snapshotId); - dispatch(setSelectSnapshotId(snapshotId === CURRENT_ITEM_KEY ? "" : snapshotId)); + dispatch(setSelectSnapshotId( + snapshotId === CURRENT_ITEM_KEY ? "" : snapshotId, + isArchivedSnapshot, + )); if (snapshotId === CURRENT_ITEM_KEY) { setAppInfo(currentAppInfo); return; @@ -202,56 +219,108 @@ export const AppSnapshot = React.memo((props: { currentAppInfo: AppSummaryInfo } return; } dispatch( - fetchSnapshotDslAction(application.applicationId, snapshotId, (dsl) => { - setAppInfo((i) => ({ - ...i, - dsl: dsl.applicationsDsl, - moduleDsl: dsl.moduleDSL, - })); - }) + fetchSnapshotDslAction( + application.applicationId, + snapshotId, + isArchivedSnapshot, + (dsl) => { + setAppInfo((i) => ({ + ...i, + dsl: dsl.applicationsDsl, + moduleDsl: dsl.moduleDSL, + })); + } + ) ); }, - [application, currentAppInfo, dispatch, setAppInfo, selectedItemKey] + [application, currentAppInfo, dispatch, setAppInfo, selectedItemKey, activeTab] ); - let snapShotContent; - if (snapshotsFetching || (currentPage === 1 && appSnapshots.length > 0 && !latestDsl)) { - snapShotContent = ; - } else if (appSnapshots.length <= 0 || !application) { - snapShotContent = ; - } else { - let snapshotItems: SnapshotItemProps[] = appSnapshots.map((snapshot, index) => { - return { - selected: selectedItemKey === snapshot.snapshotId, - title: - `${ - !latestDslChanged && currentPage === 1 && index === 0 - ? trans("history.currentVersionWithBracket") - : "" - }` + getOperationDesc(snapshot.context), - timeInfo: timestampToHumanReadable(snapshot.createTime), - userName: snapshot.userName, - onClick: () => { - onSnapshotItemClick(snapshot.snapshotId); - }, - }; - }); - if (currentPage === 1 && latestDslChanged) { - snapshotItems = [ - { - selected: selectedItemKey === CURRENT_ITEM_KEY, - title: trans("history.currentVersion"), - timeInfo: trans("history.justNow"), - userName: user.username, + const snapShotContent = useMemo(() => { + if (snapshotsFetching || (currentPage === 1 && appSnapshots.length > 0 && !latestDsl)) { + return ; + } else if (appSnapshots.length <= 0 || !application) { + return ; + } else { + let snapshotItems: SnapshotItemProps[] = appSnapshots.map((snapshot, index) => { + return { + selected: selectedItemKey === snapshot.snapshotId, + title: + `${ + !latestDslChanged && currentPage === 1 && index === 0 + ? trans("history.currentVersionWithBracket") + : "" + }` + getOperationDesc(snapshot.context), + timeInfo: timestampToHumanReadable(snapshot.createTime), + userName: snapshot.userName, onClick: () => { - onSnapshotItemClick(CURRENT_ITEM_KEY); + onSnapshotItemClick(snapshot.snapshotId); + }, + }; + }); + if (currentPage === 1 && latestDslChanged) { + snapshotItems = [ + { + selected: selectedItemKey === CURRENT_ITEM_KEY, + title: trans("history.currentVersion"), + timeInfo: trans("history.justNow"), + userName: user.username, + onClick: () => { + onSnapshotItemClick(CURRENT_ITEM_KEY); + }, }, - }, - ...snapshotItems, - ]; + ...snapshotItems, + ]; + } + return ; } - snapShotContent = ; - } + }, [ + user, + snapshotsFetching, + currentPage, + appSnapshots, + latestDsl, + application, + selectedItemKey, + latestDslChanged, + onSnapshotItemClick, + ]); + + const TabContent = useMemo(() => ( + <> + + {snapShotContent} + + + { + setCurrentPage(page); + fetchSnapshotList(page); + }} + total={totalCount} + pageSize={PAGE_SIZE} + showSizeChanger={false} + /> + + + ), [headerHeight, footerHeight, snapShotContent, currentPage, totalCount]); + + const tabConfigs = useMemo(() => [ + { + key: "recent", + title: "Recent", + icon: , + content: TabContent, + }, + { + key: "archive", + title: "Archive", + icon: , + content: TabContent, + } + ], [TabContent]); return ( }> @@ -262,31 +331,13 @@ export const AppSnapshot = React.memo((props: { currentAppInfo: AppSummaryInfo } compInstance={compInstance} /> - - - {trans("history.history")} - { - dispatch(setShowAppSnapshot(false)); - }} - /> - - - {snapShotContent} - - - { - setCurrentPage(page); - fetchSnapshotList(page); - }} - total={totalCount} - pageSize={PAGE_SIZE} - showSizeChanger={false} - /> - + { + setActiveTab(key); + }} + tabsConfig={tabConfigs} + activeKey={activeTab} + /> ); diff --git a/client/packages/lowcoder/src/pages/editor/bottom/BottomSidebar.tsx b/client/packages/lowcoder/src/pages/editor/bottom/BottomSidebar.tsx index 1e75ec1416..03ff67c754 100644 --- a/client/packages/lowcoder/src/pages/editor/bottom/BottomSidebar.tsx +++ b/client/packages/lowcoder/src/pages/editor/bottom/BottomSidebar.tsx @@ -323,7 +323,7 @@ const HighlightBorder = styled.div<{ $active: boolean; $foldable: boolean; $leve max-width: 100%; flex: 1; display: flex; - padding-left: ${(props) => props.$level * 20 + (props.$foldable ? 0 : 14)}px; + padding-left: ${(props) => props.$level * 10 + (props.$foldable ? 0 : 14)}px; border-radius: 4px; border: 1px solid ${(props) => (props.$active ? BorderActiveColor : "transparent")}; align-items: center; diff --git a/client/packages/lowcoder/src/pages/editor/right/ModulePanel.tsx b/client/packages/lowcoder/src/pages/editor/right/ModulePanel.tsx index dc4ad3cc91..a24b787d2a 100644 --- a/client/packages/lowcoder/src/pages/editor/right/ModulePanel.tsx +++ b/client/packages/lowcoder/src/pages/editor/right/ModulePanel.tsx @@ -1,89 +1,234 @@ -import CreateAppButton from "components/CreateAppButton"; -import { EmptyContent } from "components/EmptyContent"; -import { ApplicationMeta, AppTypeEnum } from "constants/applicationConstants"; -import { APPLICATION_VIEW_URL } from "constants/routesURL"; +import { ApplicationMeta, AppTypeEnum, FolderMeta } from "constants/applicationConstants"; import { - ActiveTextColor, - BorderActiveShadowColor, - BorderColor, - GreyTextColor, + BorderActiveColor, + NormalMenuIconColor, } from "constants/style"; -import { ModuleDocIcon } from "lowcoder-design"; -import { trans } from "i18n"; +import { APPLICATION_VIEW_URL } from "constants/routesURL"; +import { RightContext } from "./rightContext"; +import { + EditPopover, + EditText, + FoldedIcon, + ModuleDocIcon, + PointIcon, + PopupCard, + UnfoldIcon, + FileFolderIcon, messageInstance, CustomModal +} from "lowcoder-design"; +import {trans, transToNode} from "i18n"; import { draggingUtils } from "layout/draggingUtils"; -import { useContext, useEffect } from "react"; +import React, { useContext, useEffect, useState} from "react"; import { useDispatch, useSelector } from "react-redux"; -import { fetchAllModules } from "redux/reduxActions/applicationActions"; +import {fetchAllModules, recycleApplication, updateAppMetaAction} from "redux/reduxActions/applicationActions"; import styled from "styled-components"; +import CreateAppButton from "components/CreateAppButton"; import { TransparentImg } from "util/commonUtils"; -import { ExternalEditorContext } from "util/context/ExternalEditorContext"; -import { formatTimestamp } from "util/dateTimeUtils"; -import { RightContext } from "./rightContext"; -import { modulesSelector } from "../../../redux/selectors/applicationSelector"; -import { ComListTitle, ExtensionContentWrapper } from "./styledComponent"; - +import { ComListTitle } from "./styledComponent"; +import {folderElementsSelector} from "@lowcoder-ee/redux/selectors/folderSelector"; +import {DraggableTree} from "@lowcoder-ee/components/DraggableTree/DraggableTree"; +import { showAppSnapshotSelector} from "@lowcoder-ee/redux/selectors/appSnapshotSelector"; +import {DraggableTreeNode, DraggableTreeNodeItemRenderProps} from "@lowcoder-ee/components/DraggableTree/types"; +import { EmptyContent } from "components/EmptyContent"; +import {deleteFolder, moveToFolder, updateFolder} from "@lowcoder-ee/redux/reduxActions/folderActions"; const ItemWrapper = styled.div` + display: flex; + flex-direction: row; + &:last-child { + margin-bottom: 0; + } + .module-container { + display: flex; + width: 195px; + } + .module-icon { + margin-right: 4px; + width:19px; + height: 19px; + } + .module-content { + flex: 1; display: flex; - flex-direction: row; - margin-bottom: 12px; - &:last-child { - margin-bottom: 0; + flex-direction: column; + justify-content: space-around; + overflow: hidden; + } + .module-name { + //flex-grow: 1; + //margin-right: 8px; + line-height: 1.5; + font-size: 13px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } +`; + +type NodeType = { + name: string; + id: string; + isFolder: boolean; + containerSize?: { height: number; width: number }; + module?: ApplicationMeta; + children: NodeType[]; + rename: (val: string) => string + checkName: (val: string) => string +}; + + + +function buildTree(elementRecord: Record>): NodeType { + const elements = elementRecord[""]; + const elementMap: Record = {}; + let rootNode: NodeType = { + name: "root", + id: "", + isFolder: true, + children: [], + rename: val => rootNode.name = val, + checkName: val => val } - &:hover { - cursor: grab; - .module-icon { - box-shadow: 0 0 5px 0 rgba(49, 94, 251, 0.15); - border-color: ${BorderActiveShadowColor}; - transform: scale(1.2); - } - .module-name { - color: ${ActiveTextColor}; + + // Initialize all folders and applications as NodeType + for (const element of elements) { + if (element.folder) { + elementMap[element.folderId] = { + name: element.name, + id: element.folderId, + isFolder: true, + children: [], + rename: val => elementMap[element.folderId].name = val, + checkName: val => val + }; + + // Process subapplications inside the folder + for (const app of element.subApplications || []) { + if (!!app && app.applicationType === AppTypeEnum.Module) { + const appNode: NodeType = { + name: app.name, + id: app.applicationId, + containerSize: app.containerSize, + isFolder: false, + children: [], + module: app, + rename: val => appNode.name = val, + checkName: val => val + }; + elementMap[element.folderId].children.push(appNode); // Add applications as children of the folder + } + } + } else { + if (element.applicationType === AppTypeEnum.Module) { + elementMap[element.applicationId] = { + name: element.name, + containerSize: element.containerSize, + id: element.applicationId, + isFolder: false, + children: [], + module: element, + rename: val => elementMap[element.applicationId].name = val, + checkName: val => val + }; + } } } - .module-icon { - transition: all 200ms linear; - margin-right: 8px; - width: 40px; - height: 40px; - display: flex; - justify-content: center; - align-items: center; - border: 1px solid ${BorderColor}; - border-radius: 4px; - } - .module-content { - flex: 1; - display: flex; - flex-direction: column; - justify-content: space-around; - overflow: hidden; - } - .module-name { - line-height: 1.5; - font-size: 13px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - .module-desc { - line-height: 1.5; - font-size: 12px; - color: ${GreyTextColor}; + + // Build the tree structure + for (const element of elements) { + if (element.folder) { + const parentId = element.parentFolderId; + if (parentId && elementMap[parentId]) { + elementMap[parentId].children.push(elementMap[element.folderId]); + } else { + rootNode.children.push(elementMap[element.folderId]); + } + } else if (elementMap[element.applicationId]) { + rootNode.children.push(elementMap[element.applicationId]); + } } -`; + rootNode.children.sort((a, b) => { + if (a.isFolder && !b.isFolder) { + return -1; // a is a isFolder and should come first + } else if (!a.isFolder && b.isFolder) { + return 1; // b is a folder and should come first + } else { + return 0; // both are folders or both are not, keep original order + } + }); + return rootNode; +} + interface ModuleItemProps { meta: ApplicationMeta; onDrag: (type: string) => void; + isOverlay: boolean; + selectedID: string; + setSelectedID: (id: string) => void; + selectedType: boolean; + setSelectedType: (id: boolean) => void; + resComp: NodeType; + id: string; + $level: number; } function ModuleItem(props: ModuleItemProps) { const compType = "module"; - const { meta } = props; + const { + meta , + isOverlay, + selectedID, + setSelectedID, + selectedType, + setSelectedType, + resComp, + id, + $level, + } = props; + const dispatch = useDispatch(); + const type = resComp.isFolder; + const name = resComp.name; + const [error, setError] = useState(undefined); + const [editing, setEditing] = useState(false); + const readOnly = useSelector(showAppSnapshotSelector); + const isSelected = type === selectedType && id === selectedID; + const handleFinishRename = (value: string) => { + if (value !== "") { + let success = false; + let compId = name; + if (resComp.rename) { + compId = resComp.rename(value); + success = !!compId; + } else { + compId = name; + success = true; + } + if (success) { + setSelectedID(compId); + setSelectedType(type); + setError(undefined); + try { + dispatch(updateAppMetaAction({ + applicationId: selectedID, + name: value + })); + } catch (error) { + console.error("Error: Rename module in extension:", error); + throw error; + } + } + setError(undefined); + } + setError(undefined); + }; + + const handleNameChange = (value: string) => { + value === "" ? setError("Cannot Be Empty") : setError(undefined); + }; return ( { + e.stopPropagation(); e.dataTransfer.setData("compType", compType); e.dataTransfer.setDragImage(TransparentImg, 0, 0); draggingUtils.setData("compType", compType); @@ -99,58 +244,503 @@ function ModuleItem(props: ModuleItemProps) { props.onDrag(compType); }} > -
- -
-
-
{props.meta.name}
-
{formatTimestamp(props.meta.createAt)}
+
+ +
+ setEditing(editing)} + /> + +
); } +const HighlightBorder = styled.div<{ $active: boolean; $foldable: boolean; $level: number }>` + max-width: 100%; + flex: 1; + display: flex; + padding-left: ${(props) => props.$level * 10 + (props.$foldable ? 0 : 14)}px; + border-radius: 4px; + border: 1px solid ${(props) => (props.$active ? BorderActiveColor : "transparent")}; + align-items: center; + justify-content: space-between; +`; + +interface ColumnDivProps { + $color?: boolean; + $isOverlay: boolean; +} + +const ColumnDiv = styled.div` + width: 100%; + height: 25px; + display: flex; + user-select: none; + padding-left: 2px; + padding-right: 15px; + background-color: ${(props) => (props.$isOverlay ? "rgba(255, 255, 255, 0.11)" : "")}; + + &&& { + background-color: ${(props) => (props.$color && !props.$isOverlay ? "#f2f7fc" : null)}; + } + + &:hover { + background-color: #f2f7fc80; + cursor: pointer; + } + + .taco-edit-text-wrapper { + width: 100%; + height: 21px; + line-height: 21px; + color: #222222; + margin-left: 0; + font-size: 13px; + padding-left: 0; + + &:hover { + background-color: transparent; + } + } + + .taco-edit-text-input { + width: 100%; + height: 21px; + line-height: 21px; + color: #222222; + margin-left: 0; + font-size: 13px; + background-color: #fdfdfd; + border: 1px solid #3377ff; + border-radius: 2px; + + &:focus { + border-color: #3377ff; + box-shadow: 0 0 0 2px #d6e4ff; + } + } +`; + +const FoldIconBtn = styled.div` + width: 12px; + height: 12px; + display: flex; + margin-right: 2px; +`; + +const Icon = styled(PointIcon)` + width: 16px; + height: 16px; + cursor: pointer; + flex-shrink: 0; + color: ${NormalMenuIconColor}; + + &:hover { + color: #315efb; + } +`; + +interface ModuleSidebarItemProps extends DraggableTreeNodeItemRenderProps { + id: string; + resComp: NodeType; + onCopy: () => void; + onSelect: () => void; + onDelete: () => void; + onToggleFold: () => void; + selectedID: string; + setSelectedID: (id: string) => void; + selectedType: boolean; + 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 { + id, + resComp, + isOver, + isOverlay, + path, + isFolded, + selectedID, + setSelectedID, + selectedType, + setSelectedType, + onDelete, + onCopy, + onSelect, + onToggleFold, + } = props; + const { onDrag } = useContext(RightContext); + const [error, setError] = useState(undefined); + const [editing, setEditing] = useState(false); + const readOnly = useSelector(showAppSnapshotSelector); + const level = path.length - 1; + const type = resComp.isFolder; + const name = resComp.name; + const isSelected = type === selectedType && id === selectedID; + const isFolder = type; + + const handleFinishRename = (value: string) => { + if (value !== ""){ + let success = false; + let compId = name; + if (resComp.rename) { + compId = resComp.rename(value); + success = !!compId; + } else { + compId = name; + success = true; + } + if (success) { + setSelectedID(compId); + setSelectedType(type); + setError(undefined); + try{ + dispatch(updateFolder({ id: selectedID, name: value })); + } catch (error) { + console.error("Error: Delete module in extension:", error); + throw error; + } + + } + setError(undefined); + } + }; + + const handleNameChange = (value: string) => { + value === "" ? setError("Cannot Be Empty") : setError(undefined); + }; + + const handleClickItem = () => { + if (isFolder) { + onToggleFold(); + } + onSelect(); + }; + + return ( + + + {isFolder && {!isFolded ? : }} + { isFolder ? + <> + +
+ setEditing(editing)} + /> + +
+ : + } + {!readOnly && !isOverlay && ( + onDelete()}> + + + )} +
+
+ ); +} + export default function ModulePanel() { const dispatch = useDispatch(); - const modules = useSelector(modulesSelector); - const { onDrag, searchValue } = useContext(RightContext); - const { applicationId } = useContext(ExternalEditorContext); + let elements = useSelector(folderElementsSelector); + const { searchValue } = useContext(RightContext); + const [selectedID, setSelectedID] = useState(""); + const [selectedType, setSelectedType] = useState(false); + let sourceFolderId : string = ""; + let sourceId : string = ""; + let folderId : string = ""; + const tree = buildTree(elements); + const getById = (id: string): NodeType | undefined => getByIdFromNode(tree, id); + let popedItemSourceId = ""; useEffect(() => { dispatch(fetchAllModules({})); }, [dispatch]); - const filteredModules = modules.filter((i) => { - if (i.applicationId === applicationId || i.applicationType !== AppTypeEnum.Module) { - return false; + const moveModule = () => { + try{ + if (sourceId !== "") { + dispatch( + moveToFolder( + { + sourceFolderId: sourceFolderId!, + sourceId: sourceId!, + folderId: folderId!, + moveFlag: true + }, + () => { + + + }, + () => {} + ) + ); + } + } catch (error) { + console.error("Error: Move module in extension:", error); + throw error; + } finally { + folderId = ""; + sourceId = ""; + sourceFolderId = ""; + } + + } + + const getByIdFromNode = (root: NodeType | null, id: string): NodeType | undefined => { + if (!root) { + return; } - return i.name?.toLowerCase()?.includes(searchValue.trim()?.toLowerCase()) || !searchValue?.trim(); - }); - const items = filteredModules.map((i) => ( - - )); - const empty = ( - -

{trans("rightPanel.emptyModules")}

- { - const appId = app.applicationInfoView.applicationId; - const url = APPLICATION_VIEW_URL(appId, "edit"); - window.open(url); - }} - /> - + if (root.id === id) { + return root; + } + + for (const child of root.children) { + const result = getByIdFromNode(child, id); + if (result) { + return result; } - /> - ); + } + return; + } + const convertRefTree = (treeNode: NodeType) => { //Convert elements into tree + const moduleResComp = getById(treeNode.id); + const currentNodeType = moduleResComp?.isFolder; + + const childrenItems = treeNode.children + .map((i) => convertRefTree(i as NodeType)) + .filter((i): i is DraggableTreeNode => !!i); + const node: DraggableTreeNode = { + id: moduleResComp?.id, + canDropBefore: (source) => { + if (currentNodeType) { + return source?.isFolder!; + } + + return !source?.isFolder; + }, + canDropAfter: (source) => { + if ( + !currentNodeType && + source?.isFolder + ) { + return false; + } + return true; + }, + canDropIn: (source) => { + if (!currentNodeType) { + return false; + } + if (!source) { + return true; + } + if (source.isFolder) { + return false; + } + return true; + }, + items: childrenItems, + data: moduleResComp, + addSubItem(value) { + folderId = node.id!; + moveModule(); + }, + deleteItem(index) { + sourceFolderId = node.id!; + sourceId = node.items[index].id!; + + }, + addItem(value) { + folderId = node.id!; + moveModule(); + }, + moveItem(from, to) { + }, + }; + + if ( + searchValue && + moduleResComp && + !moduleResComp.name.toLowerCase().includes(searchValue.toLowerCase()) && + childrenItems.length === 0 + ) { + return; + } + return node; + }; + const node = convertRefTree(tree); + function onCopy(type: boolean, id: string) { + } + + function onSelect(type: boolean, id: string, meta: any) { + setSelectedID(id); + setSelectedType(type); + } + + function onDelete(type: boolean, id: string, node: NodeType) { + if (type) { + if (node.children.length) { + messageInstance.error(trans("module.folderNotEmpty")) + } else { + try { + dispatch( + deleteFolder( + {folderId: id, parentFolderId: ""}, + () => { + messageInstance.success(trans("home.deleteSuccessMsg")); + }, + () => { + messageInstance.error(trans("error")) + } + ) + ); + } catch (error) { + console.error("Error: Remove folder in extension:", error); + throw error; + } + } + } else { + try { + CustomModal.confirm({ + title: trans("home.moveToTrash"), + content: transToNode("home.moveToTrashSubTitle", { + type: "", + name: "This file", + }), + onConfirm: () => { + dispatch( + recycleApplication( + { + applicationId: id, + folderId: popedItemSourceId, + }, + () => { + messageInstance.success(trans("success")); + + }, + () => { + messageInstance.error(trans("error")); + } + ) + ) + }, + confirmBtnType: "delete", + okText: trans("home.moveToTrash"), + onCancel: () => {} + }); + } catch (error) { + console.error("Error: Remove module in extension:", error); + throw error; + } + } + } return ( <> {trans("rightPanel.moduleListTitle")} - {items.length > 0 ? items : empty} + {node?.items.length ? + node={node!} + disable={!!searchValue} + unfoldAll={!!searchValue} + showSubInDragOverlay={false} + showDropInPositionLine={false} + showPositionLineDot + positionLineDotDiameter={4} + positionLineHeight={1} + itemHeight={25} + positionLineIndent={(path, dropInAsSub) => { + const indent = 2 + (path.length - 1) * 30; + if (dropInAsSub) { + return indent + 12; + } + return indent; + }} + renderItemContent={(params) => { + const { node, onToggleFold, onDelete: onDeleteTreeItem, ...otherParams } = params; + const resComp = node.data; + if (!resComp) { + return null; + } + const id = resComp.id; + const isFolder = resComp.isFolder; + return ( + onCopy(isFolder, id)} + onSelect={() => onSelect(isFolder, id, resComp)} + selectedID={selectedID} + setSelectedID={setSelectedID} + selectedType={selectedType} + setSelectedType={setSelectedType} + onDelete={() => { + (onDelete(isFolder, id, resComp)) + }} + {...otherParams} + /> + ); + }} + /> : empty} ); } diff --git a/client/packages/lowcoder/src/pages/queryLibrary/LeftNav.tsx b/client/packages/lowcoder/src/pages/queryLibrary/LeftNav.tsx index 949a41fa03..84bdade673 100644 --- a/client/packages/lowcoder/src/pages/queryLibrary/LeftNav.tsx +++ b/client/packages/lowcoder/src/pages/queryLibrary/LeftNav.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import {useEffect, useState} from "react"; import styled, { css } from "styled-components"; import { BluePlusIcon, @@ -21,6 +21,7 @@ import { trans } from "i18n"; import { DatasourceType } from "@lowcoder-ee/constants/queryConstants"; import { saveAs } from "file-saver"; import DataSourceIcon from "components/DataSourceIcon"; +import PaginationComp from "@lowcoder-ee/util/pagination/Pagination"; const Wrapper = styled.div<{ $readOnly?: boolean }>` display: flex; @@ -72,7 +73,7 @@ const CreateBtn = styled(TacoButton)<{ $readOnly?: boolean }>` `; const Body = styled.div` - height: calc(100% - 80px); + height: calc(100% - 120px); display: flex; flex-direction: column; `; @@ -158,11 +159,31 @@ export const LeftNav = (props: { addQuery: () => void; onSelect: (queryId: string) => void; readOnly?: boolean; + setCurrentPage: (page: number) => void; + setPageSize: (size: number) => void; + currentPage: number; + pageSize: number; + total: number; + setSearchValues: any; + searchValues: string; + setModify?: any; + modify?: boolean; }) => { + const {currentPage, setCurrentPage, pageSize, setPageSize, total , setSearchValues, searchValues, modify, setModify} = props const dispatch = useDispatch(); const [searchValue, setSearchValue] = useState(""); const datasourceTypes = useSelector(getDataSourceTypesMap); + useEffect(()=> { + const timer = setTimeout(() => { + if (searchValue.length > 2 || searchValue === "") + setSearchValues(searchValue) + }, 500); + return () => clearTimeout(timer); + }, [searchValue]) + + + return ( @@ -189,12 +210,6 @@ export const LeftNav = (props: { let datasourceTypeName = datasourceTypes[q.libraryQueryDSL?.query?.compType as DatasourceType]?.name ?? ""; - if (searchValue) { - return ( - q.name.toLowerCase().includes(searchValue) || - datasourceTypeName.toLowerCase().includes(searchValue) - ); - } return true; }) .map((q) => ( @@ -234,8 +249,12 @@ export const LeftNav = (props: { CustomModal.confirm({ title: trans("queryLibrary.deleteQueryTitle"), content: trans("queryLibrary.deleteQueryContent"), - onConfirm: () => - dispatch(deleteQueryLibrary({ queryLibraryId: q.id })), + onConfirm: () => { + dispatch(deleteQueryLibrary({ queryLibraryId: q.id })) + setTimeout(() => { + setModify(!modify); + }, 200); + }, confirmBtnType: "delete", okText: trans("delete"), }), @@ -272,6 +291,17 @@ export const LeftNav = (props: { + ); diff --git a/client/packages/lowcoder/src/pages/queryLibrary/QueryLibraryEditor.tsx b/client/packages/lowcoder/src/pages/queryLibrary/QueryLibraryEditor.tsx index 9882c360a1..d331b568aa 100644 --- a/client/packages/lowcoder/src/pages/queryLibrary/QueryLibraryEditor.tsx +++ b/client/packages/lowcoder/src/pages/queryLibrary/QueryLibraryEditor.tsx @@ -22,7 +22,7 @@ import { useCompInstance } from "../../comps/utils/useCompInstance"; import { QueryLibraryComp } from "../../comps/comps/queryLibrary/queryLibraryComp"; import { useSearchParam, useThrottle } from "react-use"; import { Comp } from "lowcoder-core"; -import { LibraryQuery } from "../../api/queryLibraryApi"; +import {LibraryQuery} from "../../api/queryLibraryApi"; import { NameGenerator } from "../../comps/utils"; import { QueryLibraryHistoryView } from "./QueryLibraryHistoryView"; import { default as Form } from "antd/es/form"; @@ -46,6 +46,7 @@ import { importQueryLibrary } from "./importQueryLibrary"; import { registryDataSourcePlugin } from "constants/queryConstants"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { Helmet } from "react-helmet"; +import {fetchQLPaginationByOrg} from "@lowcoder-ee/util/pagination/axios"; const Wrapper = styled.div` display: flex; @@ -59,9 +60,21 @@ const RightContent = styled.div` position: relative; `; +interface ElementsState { + elements: LibraryQuery[]; + total: number; +} + +function transformData(input: LibraryQuery[]) { + const output: any = {}; + input.forEach(item => { + output[item.id] = item; + }); + return output; +} + export const QueryLibraryEditor = () => { const dispatch = useDispatch(); - const queryLibrary = useSelector(getQueryLibrary); const queryLibraryRecords = useSelector(getQueryLibraryRecords); const originDatasourceInfo = useSelector(getDataSource); const currentUser = useSelector(getUser); @@ -74,6 +87,12 @@ export const QueryLibraryEditor = () => { const [publishModalVisible, setPublishModalVisible] = useState(false); const [showHistory, setShowHistory] = useState(false); const [isDataSourceReady, setIsDataSourceReady] = useState(false); + const [elements, setElements] = useState({ elements: [], total: 0 }); + const [queryLibrary, setQueryLibrary] = useState({}); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [searchValues, setSearchValues] = useState(""); + const [modify, setModify] = useState(false); const selectedRecords = queryLibraryRecords[selectedQuery] ?? {}; const libraryQuery = queryLibrary[selectedQuery]; @@ -98,10 +117,33 @@ export const QueryLibraryEditor = () => { const [comp, container] = useCompInstance(params); useSaveQueryLibrary(libraryQuery, comp); + useEffect(() => { + try { + fetchQLPaginationByOrg( + { + name: searchValues, + pageNum: currentPage, + pageSize: pageSize, + } + ).then(result => { + if (result.success){ + setElements({elements: result.data || [], total: result.total || 1}) + setQueryLibrary(transformData(result.data || [])); + } + }); + } catch (error) { + console.error(error) + } + }, [currentPage, pageSize, searchValues, modify]) + + useEffect( () => { + if (searchValues !== "") + setCurrentPage(1); + }, [searchValues] + ); + useEffect(() => { if (orgId) { - dispatch(fetchQueryLibrary()); - dispatch(fetchDataSourceTypes({ organizationId: orgId })); dispatch( fetchDatasource({ organizationId: orgId, @@ -125,7 +167,8 @@ export const QueryLibraryEditor = () => { useEffect(() => { if (!forwardQueryId && !queryLibrary[selectedQuery]) { - setSelectedQuery(Object.values(queryLibrary)?.[0]?.id); + // @ts-ignore + setSelectedQuery(Object.values(queryLibrary)?.[0]?.id); } }, [dispatch, Object.keys(queryLibrary).length]); @@ -145,13 +188,13 @@ export const QueryLibraryEditor = () => { }) .map((info) => info.datasource); - const recentlyUsed = Object.values(queryLibrary) - .map((i) => i.libraryQueryDSL?.query.datasourceId) + const recentlyUsed = Object.values(queryLibrary) + .map((i: any) => i.libraryQueryDSL?.query.datasourceId) .map((id) => datasource.find((d) => d.id === id)) .filter((i) => !!i) as Datasource[]; const nameGenerator = new NameGenerator(); - nameGenerator.init(Object.values(queryLibrary).map((t) => t.name)); + nameGenerator.init(Object.values(queryLibrary).map((t: any) => t.name)); const newName = nameGenerator.genItemName(trans("queryLibrary.unnamed")); const handleAdd = (type: BottomResTypeEnum, extraInfo?: any) => { @@ -170,6 +213,11 @@ export const QueryLibraryEditor = () => { }, (resp) => { setSelectedQuery(resp.data.data.id); + setTimeout(() => { + setModify(!modify); + }, 200); + setCurrentPage(Math.ceil(elements.total / pageSize)); + }, () => {} ) @@ -189,7 +237,16 @@ export const QueryLibraryEditor = () => { setSelectedQuery(id); showCreatePanel(false); } } - readOnly={showHistory} /> + setCurrentPage={setCurrentPage} + setPageSize={setPageSize} + currentPage={currentPage} + pageSize={pageSize} + total={elements.total} + setSearchValues={setSearchValues} + searchValues={searchValues} + setModify={setModify} + modify={modify} + /> {!selectedQuery || !comp?.children.query.children.id.getView() ? ( EmptyQueryWithoutTab @@ -202,6 +259,8 @@ export const QueryLibraryEditor = () => { comp.propertyView({ onPublish: () => setPublishModalVisible(true), onHistoryShow: () => setShowHistory(true), + setModify: setModify, + modify: modify }) )} @@ -219,6 +278,10 @@ export const QueryLibraryEditor = () => { onSuccess: (resp) => { setSelectedQuery(resp.data.data.id); showCreatePanel(false); + setTimeout(() => { + setModify(!modify); + }, 200); + setCurrentPage(Math.ceil(elements.total / pageSize)); }, })} /> )} diff --git a/client/packages/lowcoder/src/pages/setting/permission/addGroupUserDialog.tsx b/client/packages/lowcoder/src/pages/setting/permission/addGroupUserDialog.tsx index b49d22199f..726308be98 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/addGroupUserDialog.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/addGroupUserDialog.tsx @@ -31,8 +31,10 @@ function AddGroupUserDialog(props: { orgUsersFetching: boolean; groupUsers: GroupUser[]; style?: CSSProperties; + setModify?: any; + modify?: boolean }) { - const { orgId, orgUsers, orgUsersFetching, groupUsers, groupId } = props; + const { orgId, orgUsers, orgUsersFetching, groupUsers, groupId, setModify, modify } = props; const groupUserIdMap = new Map(groupUsers.map((gUser) => [gUser.userId, gUser])); const [dialogVisible, setDialogVisible] = useState(false); const addableUsers = orgUsers.filter((user) => !groupUserIdMap.has(user.userId)); @@ -83,6 +85,9 @@ function AddGroupUserDialog(props: { } } dispatch(fetchGroupUsersAction({ groupId })); + setTimeout(() => { + setModify(!modify); + }, 200); setDialogVisible(false); }} > diff --git a/client/packages/lowcoder/src/pages/setting/permission/groupUsersPermission.tsx b/client/packages/lowcoder/src/pages/setting/permission/groupUsersPermission.tsx index c0f7c79d8e..4ed3e0a3c8 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/groupUsersPermission.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/groupUsersPermission.tsx @@ -3,16 +3,13 @@ import { User } from "constants/userConstants"; import { AddIcon, ArrowIcon, CustomSelect, PackUpIcon, SuperUserIcon } from "lowcoder-design"; import { trans } from "i18n"; import ProfileImage from "pages/common/profileImage"; -import React, { useEffect, useMemo } from "react"; -import { connect, useDispatch } from "react-redux"; -import { AppState } from "redux/reducers"; +import React, { useMemo } from "react"; +import { useDispatch } from "react-redux"; import { deleteGroupUserAction, - fetchGroupUsersAction, quitGroupAction, updateUserGroupRoleAction, } from "redux/reduxActions/orgActions"; -import { getUser } from "redux/selectors/usersSelectors"; import styled from "styled-components"; import { formatTimestamp } from "util/dateTimeUtils"; import { currentOrgAdmin, isGroupAdmin } from "util/permissionUtils"; @@ -44,14 +41,15 @@ type GroupPermissionProp = { group: OrgGroup; orgId: string; groupUsers: GroupUser[]; - groupUsersFetching: boolean; currentUserGroupRole: string; currentUser: User; + setModify?: any; + modify?: boolean; }; function GroupUsersPermission(props: GroupPermissionProp) { const { Column } = TableStyled; - const { group, orgId, groupUsersFetching, groupUsers, currentUserGroupRole, currentUser } = props; + const { group, orgId, groupUsers, currentUserGroupRole, currentUser , setModify, modify} = props; const adminCount = groupUsers.filter((user) => isGroupAdmin(user.role)).length; const sortedGroupUsers = useMemo(() => { return [...groupUsers].sort((a, b) => { @@ -65,9 +63,6 @@ function GroupUsersPermission(props: GroupPermissionProp) { }); }, [groupUsers]); const dispatch = useDispatch(); - useEffect(() => { - dispatch(fetchGroupUsersAction({ groupId: group.groupId })); - }, []); return ( <> @@ -85,6 +80,8 @@ function GroupUsersPermission(props: GroupPermissionProp) { groupUsers={groupUsers} orgId={orgId} groupId={group.groupId} + setModify={setModify} + modify={modify} trigger={ }> {trans("memberSettings.addMember")} @@ -100,7 +97,7 @@ function GroupUsersPermission(props: GroupPermissionProp) { dataSource={sortedGroupUsers} rowKey="userId" pagination={false} - loading={groupUsersFetching} + loading={groupUsers.length === 0} > { + setModify(!modify); + }, 200); }} > {TacoRoles.map((role) => ( @@ -177,6 +177,9 @@ function GroupUsersPermission(props: GroupPermissionProp) { dispatch( quitGroupAction({ groupId: group.groupId, userId: currentUser.id }) ); + setTimeout(() => { + setModify(!modify); + }, 200); }} > {trans("memberSettings.exitGroup")} @@ -192,6 +195,9 @@ function GroupUsersPermission(props: GroupPermissionProp) { groupId: group.groupId, }) ); + setTimeout(() => { + setModify(!modify); + }, 200); }} > {trans("memberSettings.moveOutGroup")} @@ -208,13 +214,4 @@ function GroupUsersPermission(props: GroupPermissionProp) { ); } -const mapStateToProps = (state: AppState) => { - return { - groupUsers: state.ui.org.groupUsers, - groupUsersFetching: state.ui.org.groupUsersFetching, - currentUser: getUser(state), - currentUserGroupRole: state.ui.org.currentUserGroupRole, - }; -}; - -export default connect(mapStateToProps)(GroupUsersPermission); +export default GroupUsersPermission; diff --git a/client/packages/lowcoder/src/pages/setting/permission/index.tsx b/client/packages/lowcoder/src/pages/setting/permission/index.tsx index 8c59eaa2e6..6302d535af 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/index.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/index.tsx @@ -1,13 +1,16 @@ -import { Route, Switch } from "react-router"; +import React, {useState} from "react"; +import { Route, Switch } from "react-router-dom"; import PermissionList from "./permissionList"; import PermissionDetail from "./permissionDetail"; import { PERMISSION_SETTING, PERMISSION_SETTING_DETAIL, SETTING_URL } from "constants/routesURL"; -export default () => { +export default function PermissionRoutes() { + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); return ( - - + } /> + } /> ); -}; +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx b/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx index 992e7e0f86..e00d06e666 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx @@ -14,16 +14,13 @@ import { import { trans, transToNode } from "i18n"; import InviteDialog from "pages/common/inviteDialog"; import ProfileImage from "pages/common/profileImage"; -import React, { useEffect, useMemo } from "react"; -import { connect, useDispatch, useSelector } from "react-redux"; -import { AppState } from "redux/reducers"; +import React, { useMemo } from "react"; +import { useDispatch, useSelector } from "react-redux"; import { deleteOrgUserAction, - fetchOrgUsersAction, quitOrgAction, updateUserOrgRoleAction, } from "redux/reduxActions/orgActions"; -import { getUser } from "redux/selectors/usersSelectors"; import styled from "styled-components"; import { formatTimestamp } from "util/dateTimeUtils"; import { currentOrgAdmin } from "util/permissionUtils"; @@ -58,13 +55,14 @@ const StyledMembersIcon = styled(MembersIcon)` type UsersPermissionProp = { orgId: string; orgUsers: OrgUser[]; - orgUsersFetching: boolean; currentUser: User; + setModify?: any; + modify?: boolean; }; function OrgUsersPermission(props: UsersPermissionProp) { const { Column } = TableStyled; - const { orgId, orgUsers, orgUsersFetching, currentUser } = props; + const { orgId, orgUsers, currentUser , setModify, modify} = props; const adminCount = orgUsers.filter( (user) => user.role === ADMIN_ROLE || user.role === SUPER_ADMIN_ROLE, ).length; @@ -82,9 +80,9 @@ function OrgUsersPermission(props: UsersPermissionProp) { }); }, [orgUsers]); - useEffect(() => { - dispatch(fetchOrgUsersAction(orgId)); - }, [dispatch, orgId]); + // useEffect(() => { + // dispatch(fetchOrgUsersAction(orgId)); + // }, [dispatch, orgId]); const onResetPass = (userId: string) => { return UserApi.resetPassword(userId) @@ -151,7 +149,7 @@ function OrgUsersPermission(props: UsersPermissionProp) { dataSource={sortedOrgUsers} rowKey="userId" pagination={false} - loading={orgUsersFetching} + loading={orgUsers.length === 0} > { + setModify(!modify); + }, 200); }, confirmBtnType: "delete", okText: trans("memberSettings.moveOutOrg"), @@ -299,12 +300,4 @@ function OrgUsersPermission(props: UsersPermissionProp) { ); } -const mapStateToProps = (state: AppState) => { - return { - orgUsersFetching: state.ui.org.orgUsersFetching, - orgUsers: state.ui.org.orgUsers, - currentUser: getUser(state), - }; -}; - -export default connect(mapStateToProps)(OrgUsersPermission); +export default OrgUsersPermission; diff --git a/client/packages/lowcoder/src/pages/setting/permission/permissionDetail.tsx b/client/packages/lowcoder/src/pages/setting/permission/permissionDetail.tsx index 2e190121ef..d144c4e47e 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/permissionDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/permissionDetail.tsx @@ -1,12 +1,13 @@ -import { useEffect } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { fetchGroupsAction } from "redux/reduxActions/orgActions"; +import React, {useEffect, useState} from "react"; +import { useSelector } from "react-redux"; import { getUser } from "redux/selectors/usersSelectors"; import styled from "styled-components"; import GroupPermission from "./groupUsersPermission"; import UsersPermission from "./orgUsersPermission"; -import { getOrgGroups } from "redux/selectors/orgSelectors"; -import { useParams } from "react-router"; +import { useParams } from "react-router-dom"; +import {fetchGroupUsrPagination, fetchOrgGroups, fetchOrgUsrPagination} from "@lowcoder-ee/util/pagination/axios"; +import PaginationComp from "@lowcoder-ee/util/pagination/Pagination"; +import {OrgGroup} from "@lowcoder-ee/constants/orgConstants"; const PermissionContent = styled.div` display: flex; @@ -18,34 +19,109 @@ const PermissionContent = styled.div` width: 100%; `; -const All_Users = "users"; +export default function PermissionSetting(props: {currentPageProp: number, pageSizeProp: number}) { -export default function PermissionSetting() { + const {currentPageProp, pageSizeProp} = props; const user = useSelector(getUser); + const [elements, setElements] = useState({ elements: [], total: 1, role: "" }); + const [group, setGrouop] = useState(); + const [orgMemberElements, setOrgMemberElements] = useState({ elements: [], total: 1 }) + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [modify, setModify] = useState(false); + const orgId = user.currentOrgId; - const orgGroups = useSelector(getOrgGroups); - const groupIdMap = new Map(orgGroups.map((group) => [group.groupId, group])); - const dispatch = useDispatch(); + const currentUser = useSelector(getUser); const selectKey = useParams<{ groupId: string }>().groupId; - useEffect(() => { - if (!orgId) { - return; + + useEffect( () => { + fetchOrgGroups( + { + pageNum: currentPageProp, + pageSize: pageSizeProp, + } + ).then(result => { + if (result.success && !!result.data){ + setGrouop(result.data.find(group => group.groupId === selectKey)) + } + else + console.error("ERROR: fetchFolderElements", result.error) + }) + }, [currentPageProp, pageSizeProp] + ) + + useEffect( () => { + if (selectKey !== "users" && selectKey) + fetchGroupUsrPagination( + { + groupId:selectKey, + pageNum: currentPage, + pageSize: pageSize, + } + ).then(result => { + if (result.success){ + setElements({elements: result.data || [], total: result.total || 1, role: result.visitorRole || ""}) + } + else + console.error("ERROR: fetchFolderElements", result.error) + } + ) + else + { + fetchOrgUsrPagination( + { + orgId: orgId, + pageNum: currentPage, + pageSize: pageSize, + } + ).then(result => { + if (result.success){ + setOrgMemberElements({elements: result.data || [], total: result.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", result.error) + } + ) } - dispatch(fetchGroupsAction(orgId)); - }, [orgId]); + }, + [currentPage, pageSize, modify, selectKey] + ) + if (!orgId) { return null; } return ( - - {selectKey === All_Users ? ( - - ) : ( - groupIdMap.has(selectKey) && ( - - ) - )} - + + {selectKey === "users" ? ( + <> + + + + ) : ( + group && ( + <> + + + + + ) + )} + ); -} +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx b/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx index c72579a365..c2a5f3778b 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx @@ -21,7 +21,6 @@ import { } from "lowcoder-design"; import styled from "styled-components"; import { trans } from "i18n"; -import { getOrgGroups } from "redux/selectors/orgSelectors"; import { Table } from "components/Table"; import history from "util/history"; import { Level1SettingPageContentWithList, Level1SettingPageTitleWithBtn } from "../styled"; @@ -32,6 +31,8 @@ import { OrgGroup } from "constants/orgConstants"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import InviteDialog from "pages/common/inviteDialog"; import { Flex } from "antd"; +import {fetchOrgGroups} from "@lowcoder-ee/util/pagination/axios"; +import PaginationComp from "@lowcoder-ee/util/pagination/Pagination"; const NEW_GROUP_PREFIX = trans("memberSettings.newGroupPrefix"); @@ -51,23 +52,58 @@ type DataItemInfo = { group?: OrgGroup; }; -export default function PermissionSetting() { +type PermissionSettingProps = { + currentPage: number; + setCurrentPage: (value: number) => void; + pageSize: number; + setPageSize: (value: number) => void; +}; + +interface ElementsState { + elements: OrgGroup[]; + total: number; +} + +export default function PermissionSetting(props: PermissionSettingProps) { + + const {currentPage, setCurrentPage, pageSize, setPageSize} = props; + let dataSource: DataItemInfo[] = []; const user = useSelector(getUser); const orgId = user.currentOrgId; - const orgGroups = useSelector(getOrgGroups); - const visibleOrgGroups = orgGroups.filter((g) => !g.allUsersGroup); - const allUsersGroup = orgGroups.find((g) => g.allUsersGroup); const dispatch = useDispatch(); const [needRenameId, setNeedRenameId] = useState(undefined); const { nameSuffixFunc, menuItemsFunc, menuExtraView } = usePermissionMenuItems(orgId); const [groupCreating, setGroupCreating] = useState(false); + const [elements, setElements] = useState({ elements: [], total: 0 }); + const [modify, setModify] = useState(false); + const visibleOrgGroups = elements.elements.filter((g) => !g.allUsersGroup); + const allUsersGroup = elements.elements.find((g) => g.allUsersGroup); - useEffect(() => { - if (!orgId) { - return; - } - dispatch(fetchGroupsAction(orgId)); - }, [orgId]); + useEffect( () => { + fetchOrgGroups( + { + pageNum: currentPage, + pageSize: pageSize, + } + ).then(result => { + if (result.success){ + setElements({elements: result.data || [], total: result.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", result.error) + }) + }, [currentPage, pageSize, modify] + ) + + + dataSource = currentPage === 1 ? [{ + key: "users", + label: trans("memberSettings.allMembers"), + createTime: allUsersGroup?.createTime, + lock: true, + del: false, + rename: false, + }] : []; if (!orgId) { return null; } @@ -84,6 +120,9 @@ export default function PermissionSetting() { setTimeout(() => { dispatch(fetchGroupsAction(orgId)); }, 200); + setTimeout(() => { + setModify(!modify); + }, 200); } }) .catch((e) => { @@ -98,6 +137,9 @@ export default function PermissionSetting() { .then((resp) => { if (validateResponse(resp)) { dispatch(fetchGroupsAction(orgId)); + setTimeout(() => { + setModify(!modify); + }, 200); } }) .catch((e) => { @@ -105,17 +147,6 @@ export default function PermissionSetting() { }); }; - const dataSource: DataItemInfo[] = [ - { - key: "users", - label: trans("memberSettings.allMembers"), - createTime: allUsersGroup?.createTime, - lock: true, - del: false, - rename: false, - }, - ]; - visibleOrgGroups.forEach((group) => { dataSource.push({ key: group.groupId, @@ -180,6 +211,9 @@ export default function PermissionSetting() { return; } dispatch(updateGroupAction(record.key, { groupName: value }, orgId)); + setTimeout(() => { + setModify(!modify); + }, 200); setNeedRenameId(undefined); }, }} @@ -255,6 +289,13 @@ export default function PermissionSetting() { />
{menuExtraView} + ); } diff --git a/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx index 958995e74c..2504ca3f48 100644 --- a/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx +++ b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx @@ -2,8 +2,6 @@ import { FormInput, messageInstance, PasswordInput } from "lowcoder-design"; import { AuthBottomView, ConfirmButton, - FormWrapperMobile, - LoginCardTitle, StyledRouteLink, } from "pages/userAuth/authComponents"; import React, { useContext, useEffect, useState } from "react"; @@ -15,7 +13,7 @@ import { UserConnectionSource } from "@lowcoder-ee/constants/userConstants"; import { trans } from "i18n"; import { AuthContext, useAuthSubmit } from "pages/userAuth/authUtils"; import { ThirdPartyAuth } from "pages/userAuth/thirdParty/thirdPartyAuth"; -import { AUTH_FORGOT_PASSWORD_URL, AUTH_REGISTER_URL, ORG_AUTH_FORGOT_PASSWORD_URL, ORG_AUTH_REGISTER_URL } from "constants/routesURL"; +import { AUTH_FORGOT_PASSWORD_URL, AUTH_REGISTER_URL } from "constants/routesURL"; import { Link, useLocation, useParams } from "react-router-dom"; import { Divider } from "antd"; import Flex from "antd/es/flex"; @@ -27,8 +25,9 @@ import LeftOutlined from "@ant-design/icons/LeftOutlined"; import { fetchConfigAction } from "@lowcoder-ee/redux/reduxActions/configActions"; import { useDispatch, useSelector } from "react-redux"; import history from "util/history"; -import ApplicationApi from "@lowcoder-ee/api/applicationApi"; import { getServerSettings } from "@lowcoder-ee/redux/selectors/applicationSelector"; +import {fetchOrgPaginationByEmail} from "@lowcoder-ee/util/pagination/axios"; +import PaginationComp from "@lowcoder-ee/util/pagination/Pagination"; const StyledCard = styled.div<{$selected: boolean}>` display: flex; @@ -91,6 +90,11 @@ type FormLoginProps = { organizationId?: string; } +interface ElementsState { + elements: any; + total: number; +} + export default function FormLoginSteps(props: FormLoginProps) { const dispatch = useDispatch(); const location = useLocation(); @@ -111,6 +115,22 @@ export default function FormLoginSteps(props: FormLoginProps) { const [skipWorkspaceStep, setSkipWorkspaceStep] = useState(false); const [signupEnabled, setSignupEnabled] = useState(true); const serverSettings = useSelector(getServerSettings); + const [elements, setElements] = useState({ elements: [], total: 0 }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + useEffect(() => { + if (account) + fetchOrgPaginationByEmail({ + email: account, + pageNum: currentPage, + pageSize: pageSize + }).then( result => { + setElements({elements: result.data || [], total: result.total || 1}) + setOrgList(result.data) + } + ) + }, [pageSize, currentPage]) useEffect(() => { const { LOWCODER_EMAIL_SIGNUP_ENABLED } = serverSettings; @@ -147,20 +167,25 @@ export default function FormLoginSteps(props: FormLoginProps) { } setOrgLoading(true); - OrgApi.fetchOrgsByEmail(account) + fetchOrgPaginationByEmail({ + email: account, + pageNum: currentPage, + pageSize: pageSize + }) .then((resp) => { - if (validateResponse(resp)) { - setOrgList(resp.data.data); - if (!resp.data.data.length) { + if (resp.success) { + setElements({elements: resp.data || [], total: resp.total || 1}) + setOrgList(resp.data); + if (!resp.data.length) { history.push( AUTH_REGISTER_URL, {...location.state || {}, email: account}, ) return; } - if (resp.data.data.length === 1) { - setOrganizationId(resp.data.data[0].orgId); - dispatch(fetchConfigAction(resp.data.data[0].orgId)); + if (resp.data.length === 1) { + setOrganizationId(resp.data[0].orgId); + dispatch(fetchConfigAction(resp.data[0].orgId)); setCurrentStep(CurrentStepEnum.AUTH_PROVIDERS); return; } @@ -233,6 +258,14 @@ export default function FormLoginSteps(props: FormLoginProps) { {org.orgName} ))} + {orgList.length > 10 ? + : <>} ) diff --git a/client/packages/lowcoder/src/pages/userAuth/index.tsx b/client/packages/lowcoder/src/pages/userAuth/index.tsx index 40e7a1bc15..d33b48fdec 100644 --- a/client/packages/lowcoder/src/pages/userAuth/index.tsx +++ b/client/packages/lowcoder/src/pages/userAuth/index.tsx @@ -11,7 +11,7 @@ import { fetchConfigAction } from "redux/reduxActions/configActions"; import { fetchUserAction } from "redux/reduxActions/userActions"; import LoginAdmin from "./loginAdmin"; import _ from "lodash"; - +import {LoadingBarHideTrigger} from "@lowcoder-ee/util/hideLoading"; export default function UserAuth() { const dispatch = useDispatch(); const location = useLocation(); @@ -50,6 +50,7 @@ export default function UserAuth() { fetchUserAfterAuthSuccess, }} > + diff --git a/client/packages/lowcoder/src/redux/reducers/uiReducers/appSnapshotReducer.ts b/client/packages/lowcoder/src/redux/reducers/uiReducers/appSnapshotReducer.ts index 27d63e13f2..156f8fee54 100644 --- a/client/packages/lowcoder/src/redux/reducers/uiReducers/appSnapshotReducer.ts +++ b/client/packages/lowcoder/src/redux/reducers/uiReducers/appSnapshotReducer.ts @@ -14,6 +14,7 @@ const initialState: AppSnapshotState = { showAppSnapshot: false, snapshotDslFetching: false, selectedSnapshotId: "", + isSelectedSnapshotIdArchived: false, }; const appSnapshotReducer = createReducer(initialState, { @@ -28,11 +29,12 @@ const appSnapshotReducer = createReducer(initialState, { }, [ReduxActionTypes.SET_SELECT_SNAPSHOT_ID]: ( state: AppSnapshotState, - action: ReduxAction<{ snapshotId: string }> + action: ReduxAction<{ snapshotId: string, archived?: boolean }> ): AppSnapshotState => { return { ...state, selectedSnapshotId: action.payload.snapshotId, + isSelectedSnapshotIdArchived: action.payload.archived, }; }, [ReduxActionTypes.FETCH_APP_SNAPSHOT_DSL]: (state: AppSnapshotState): AppSnapshotState => { @@ -115,6 +117,7 @@ export interface AppSnapshotState { appSnapshotCount: number; showAppSnapshot: boolean; selectedSnapshotId: string; + isSelectedSnapshotIdArchived?: boolean; } export default appSnapshotReducer; diff --git a/client/packages/lowcoder/src/redux/reducers/uiReducers/folderReducer.ts b/client/packages/lowcoder/src/redux/reducers/uiReducers/folderReducer.ts index c27cb8d507..4326cb2ecf 100644 --- a/client/packages/lowcoder/src/redux/reducers/uiReducers/folderReducer.ts +++ b/client/packages/lowcoder/src/redux/reducers/uiReducers/folderReducer.ts @@ -37,10 +37,24 @@ export const folderReducer = createReducer(initialState, { state: FolderReduxState, action: ReduxAction ): FolderReduxState => { + const deleteArray : number[] = []; const elements = { ...state.folderElements }; - elements[action.payload.folderId ?? ""] = elements[action.payload.folderId ?? ""]?.filter( - (e) => e.folder || (!e.folder && e.applicationId !== action.payload.applicationId) - ); + elements[""] = elements[""].map((item, index) => { + if(item.folder) { + const tempSubApplications = item.subApplications?.filter(e => e.applicationId !== action.payload.applicationId); + return { ...item, subApplications: tempSubApplications }; + } else { + if (item.applicationId !== action.payload.applicationId) + return item; + else { + deleteArray.push(index); + return item; + } + } + }); + deleteArray.map(item => { + elements[""].splice(item, 1); + }) return { ...state, folderElements: elements, @@ -55,6 +69,14 @@ export const folderReducer = createReducer(initialState, { elements[action.payload.folderId ?? ""] = elements[action.payload.folderId ?? ""]?.map((e) => { if (!e.folder && e.applicationId === action.payload.applicationId) { return { ...e, ...action.payload }; + } else { + if (e.folder) { + if (e.subApplications?.map(item => { + if (item.applicationId === action.payload.applicationId) + item.name = action.payload.name + })){ + } + } } return e; }); @@ -88,7 +110,7 @@ export const folderReducer = createReducer(initialState, { action.payload.parentFolderId ?? "" ]?.map((e) => { if (e.folder && e.folderId === action.payload.folderId) { - return { ...action.payload, name: action.payload.name }; + return { ...e, name: action.payload.name}; } return e; }); @@ -107,7 +129,7 @@ export const folderReducer = createReducer(initialState, { state: FolderReduxState, action: ReduxAction ): FolderReduxState => { - const elements = { ...state.folderElements }; + let elements = { ...state.folderElements }; elements[action.payload.sourceFolderId ?? ""] = elements[ action.payload.sourceFolderId ?? "" ]?.filter( @@ -120,6 +142,59 @@ export const folderReducer = createReducer(initialState, { folderElements: elements, }; }, + [ReduxActionTypes.MOVE_TO_FOLDER2_SUCCESS]: ( + state: FolderReduxState, + action: ReduxAction + ): FolderReduxState => { + let elements = { ...state.folderElements }; + const { sourceId, folderId, sourceFolderId } = action.payload; + if(sourceFolderId === "") { + const tempItem = elements[""]?.find(e => + !e.folder && e.applicationId === sourceId + ); + elements[""] = elements[""]?.filter(e => e.folder || (e.applicationId !== sourceId)); + elements[""] = elements[""].map(item => { + if(item.folder && item.folderId === folderId && tempItem !== undefined && !tempItem.folder) { + item.subApplications?.push(tempItem); + } + return item; + }) + } else{ + let tempIndex: number | undefined; + let tempNode: any; + let temp = elements[""].map((item, index) => { + if (item.folderId === sourceFolderId && item.folder) { + const tempSubApplications = item.subApplications?.filter(e => + (e.folder && e.folderId !== sourceId) || + (!e.folder && e.applicationId !== sourceId) + ); + tempNode = item.subApplications?.filter(e => + (e.folder && e.folderId === sourceId) || + (!e.folder && e.applicationId === sourceId) + ); + return { ...item, subApplications: tempSubApplications }; + } + if (item.folderId === folderId && item.folder) { + tempIndex = index; + return item; + } + return item; + }); + if (tempIndex !== undefined) { + const targetItem = temp[tempIndex]; + if (targetItem.folder && Array.isArray(targetItem.subApplications)) { + targetItem.subApplications.push(tempNode[0]); + } + } else { + temp.push(tempNode[0]); + } + elements[""] = temp; + } + return { + ...state, + folderElements: elements, + }; + }, [ReduxActionTypes.DELETE_FOLDER_SUCCESS]: ( state: FolderReduxState, action: ReduxAction diff --git a/client/packages/lowcoder/src/redux/reduxActions/appSnapshotActions.ts b/client/packages/lowcoder/src/redux/reduxActions/appSnapshotActions.ts index 905d3a78d1..3b52b1f191 100644 --- a/client/packages/lowcoder/src/redux/reduxActions/appSnapshotActions.ts +++ b/client/packages/lowcoder/src/redux/reduxActions/appSnapshotActions.ts @@ -11,10 +11,10 @@ export const setShowAppSnapshot = (show: boolean) => { }; }; -export const setSelectSnapshotId = (snapshotId: string) => { +export const setSelectSnapshotId = (snapshotId: string, archived?: boolean) => { return { type: ReduxActionTypes.SET_SELECT_SNAPSHOT_ID, - payload: { snapshotId: snapshotId }, + payload: { snapshotId: snapshotId, archived: archived }, }; }; @@ -33,6 +33,7 @@ export const createSnapshotAction = (payload: CreateSnapshotPayload) => { export type FetchSnapshotsPayload = { applicationId: string; + archived: boolean; onSuccess?: (snapshots: AppSnapshotList) => void; } & PaginationParam; @@ -46,17 +47,24 @@ export const fetchSnapshotsAction = (payload: FetchSnapshotsPayload) => { export type FetchSnapshotDslPayload = { applicationId: string; snapshotId: string; + archived?: boolean; onSuccess: (res: AppSnapshotDslInfo) => void; }; export const fetchSnapshotDslAction = ( appId: string, snapshotId: string, + archived: boolean, onSuccess: (res: AppSnapshotDslInfo) => void ): ReduxAction => { return { type: ReduxActionTypes.FETCH_APP_SNAPSHOT_DSL, - payload: { applicationId: appId, snapshotId: snapshotId, onSuccess: onSuccess }, + payload: { + applicationId: appId, + snapshotId: snapshotId, + archived: archived, + onSuccess: onSuccess, + }, }; }; @@ -64,12 +72,14 @@ export type RecoverSnapshotPayload = { applicationId: string; snapshotId: string; snapshotCreateTime: number; + isArchivedSnapshot?: boolean; }; export const recoverSnapshotAction = ( appId: string, snapshotId: string, - snapshotCreateTime: number + snapshotCreateTime: number, + isArchivedSnapshot?: boolean, ): ReduxAction => { return { type: ReduxActionTypes.RECOVER_APP_SNAPSHOT, @@ -77,6 +87,7 @@ export const recoverSnapshotAction = ( applicationId: appId, snapshotId: snapshotId, snapshotCreateTime: snapshotCreateTime, + isArchivedSnapshot, }, }; }; diff --git a/client/packages/lowcoder/src/redux/reduxActions/folderActions.ts b/client/packages/lowcoder/src/redux/reduxActions/folderActions.ts index ba288b89a4..5c00aafe60 100644 --- a/client/packages/lowcoder/src/redux/reduxActions/folderActions.ts +++ b/client/packages/lowcoder/src/redux/reduxActions/folderActions.ts @@ -58,6 +58,7 @@ export interface MoveToFolderPayload { sourceFolderId: string; sourceId: string; folderId: string; + moveFlag?: boolean; } export const moveToFolder = ( diff --git a/client/packages/lowcoder/src/redux/sagas/appSnapshotSagas.ts b/client/packages/lowcoder/src/redux/sagas/appSnapshotSagas.ts index 266beeb5da..a111d7e84e 100644 --- a/client/packages/lowcoder/src/redux/sagas/appSnapshotSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/appSnapshotSagas.ts @@ -42,7 +42,11 @@ export function* fetchAppSnapshotsSaga(action: ReduxAction = yield call( AppSnapshotApi.getSnapshots, action.payload.applicationId, - { page: action.payload.page, size: action.payload.size } + { + page: action.payload.page, + size: action.payload.size, + }, + action.payload.archived, ); if (validateResponse(response)) { action.payload.onSuccess && action.payload.onSuccess(response.data.data); @@ -63,7 +67,8 @@ export function* fetchAppSnapshotDslSaga(action: ReduxAction = yield call( AppSnapshotApi.getSnapshotDsl, action.payload.applicationId, - action.payload.snapshotId + action.payload.snapshotId, + action.payload.archived, ); if (validateResponse(response)) { // replace dsl @@ -81,11 +86,12 @@ export function* fetchAppSnapshotDslSaga(action: ReduxAction) { try { - const { applicationId, snapshotId, snapshotCreateTime } = action.payload; + const { applicationId, snapshotId, snapshotCreateTime, isArchivedSnapshot } = action.payload; const response: AxiosResponse = yield call( AppSnapshotApi.getSnapshotDsl, applicationId, - snapshotId + snapshotId, + isArchivedSnapshot, ); if (validateResponse(response)) { // record history record diff --git a/client/packages/lowcoder/src/redux/sagas/folderSagas.ts b/client/packages/lowcoder/src/redux/sagas/folderSagas.ts index 65a39f030d..62b74659e8 100644 --- a/client/packages/lowcoder/src/redux/sagas/folderSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/folderSagas.ts @@ -84,14 +84,16 @@ export function* deleteFolderSaga(action: ReduxActionWithCallbacks) { try { + const { moveFlag } = action.payload; + delete action.payload.moveFlag; const response: AxiosResponse> = yield FolderApi.moveToFolder( action.payload ); const isValidResponse: boolean = validateResponse(response); - + const type = moveFlag ? ReduxActionTypes.MOVE_TO_FOLDER2_SUCCESS : ReduxActionTypes.MOVE_TO_FOLDER_SUCCESS; if (isValidResponse) { yield put({ - type: ReduxActionTypes.MOVE_TO_FOLDER_SUCCESS, + type, payload: action.payload, }); action.onSuccessCallback && action.onSuccessCallback(response); diff --git a/client/packages/lowcoder/src/redux/selectors/appSnapshotSelector.ts b/client/packages/lowcoder/src/redux/selectors/appSnapshotSelector.ts index c2b7af89f1..1891392508 100644 --- a/client/packages/lowcoder/src/redux/selectors/appSnapshotSelector.ts +++ b/client/packages/lowcoder/src/redux/selectors/appSnapshotSelector.ts @@ -5,9 +5,13 @@ export const showAppSnapshotSelector = (state: AppState) => { }; export const getSelectedAppSnapshot = (state: AppState) => { - return state.ui.appSnapshot.appSnapshots.find( + const selectedSnapshot = state.ui.appSnapshot.appSnapshots.find( (s) => s.snapshotId === state.ui.appSnapshot.selectedSnapshotId ); + return { + selectedSnapshot, + isArchivedSnapshot: state.ui.appSnapshot.isSelectedSnapshotIdArchived, + } }; export const appSnapshotsSelector = (state: AppState) => { diff --git a/client/packages/lowcoder/src/util/hideLoading.tsx b/client/packages/lowcoder/src/util/hideLoading.tsx new file mode 100644 index 0000000000..21ed7f34e6 --- /dev/null +++ b/client/packages/lowcoder/src/util/hideLoading.tsx @@ -0,0 +1,17 @@ +import {useEffect} from "react"; + +function hideLoading() { + // hide loading + const node = document.getElementById("loading"); + if (node) { + // @ts-ignore + node.style.opacity = 0; + } +} +export const LoadingBarHideTrigger = function(props: any) { + useEffect(() => { + setTimeout(() => hideLoading(), 300); + }, []); + + return <> +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/util/homeResUtils.tsx b/client/packages/lowcoder/src/util/homeResUtils.tsx index 1088ea01fe..89c6726347 100644 --- a/client/packages/lowcoder/src/util/homeResUtils.tsx +++ b/client/packages/lowcoder/src/util/homeResUtils.tsx @@ -7,7 +7,12 @@ import { NavDocIcon, } from "lowcoder-design"; import { HomeResTypeEnum } from "../types/homeRes"; -import { APPLICATION_VIEW_URL, APPLICATION_MARKETPLACE_VIEW_URL, buildFolderUrl } from "../constants/routesURL"; +import { + APPLICATION_VIEW_URL, + APPLICATION_MARKETPLACE_VIEW_URL, + buildFolderUrl, + ALL_APPLICATIONS_URL +} from "../constants/routesURL"; import history from "./history"; import { trans } from "../i18n"; import { FunctionComponent } from "react"; @@ -62,3 +67,5 @@ export const handleAppViewClick = (id: string) => window.open(APPLICATION_VIEW_U export const handleMarketplaceAppViewClick = (id: string, isLocalMarketplace?: boolean) => isLocalMarketplace == true ? window.open(APPLICATION_VIEW_URL(id, "view_marketplace"), '_blank') : window.open(APPLICATION_MARKETPLACE_VIEW_URL(id, "view_marketplace"), '_blank'); export const handleFolderViewClick = (id: string) => history.push(buildFolderUrl(id)); + +export const backFolderViewClick = () => history.push(ALL_APPLICATIONS_URL); \ No newline at end of file diff --git a/client/packages/lowcoder/src/util/pagination/Pagination.tsx b/client/packages/lowcoder/src/util/pagination/Pagination.tsx new file mode 100644 index 0000000000..19001dea8d --- /dev/null +++ b/client/packages/lowcoder/src/util/pagination/Pagination.tsx @@ -0,0 +1,86 @@ +import styled from "styled-components"; +import { Pagination } from "antd"; + +interface PaginationLayoutProps { + height?: number; + marginTop?: number; + marginBottom?: number; +} + +const PaginationLayout = styled(Pagination)` + display: flex; + justify-content: center; + align-items: center; + margin-top: ${(props) => props.marginTop !== undefined ? props.marginTop : 40}px !important; + margin-bottom: ${(props) => props.marginBottom !== undefined ? props.marginBottom : 20}px !important; + height: ${(props) => props.height}px; +`; + +interface PaginationCompProps { + setCurrentPage: (page: number) => void; + setPageSize: (size: number) => void; + currentPage: number; + pageSize: number; + total: number; + height?: number; + marginTop?: number; + marginBottom?: number; + simple?: boolean; +} + +const PaginationComp = (props: PaginationCompProps) => { + const { + setCurrentPage, + setPageSize, + currentPage, + pageSize, + total, + height, + marginTop, + marginBottom, + simple, + } = props; + + const handlePageChange = (page: number, pageSize: number | undefined) => { + if (setCurrentPage) { + setCurrentPage(page); + } + }; + + const handlePageSizeChange = (current: number, size: number) => { + if (setPageSize) { + setPageSize(size); + } + }; + + return ( + <> + {simple ? + : + + } + + ); +}; + +export default PaginationComp; \ No newline at end of file diff --git a/client/packages/lowcoder/src/util/pagination/axios.ts b/client/packages/lowcoder/src/util/pagination/axios.ts new file mode 100644 index 0000000000..42c0de2703 --- /dev/null +++ b/client/packages/lowcoder/src/util/pagination/axios.ts @@ -0,0 +1,170 @@ +import { FolderApi } from "@lowcoder-ee/api/folderApi"; +import ApplicationApi from "@lowcoder-ee/api/applicationApi"; +import { + fetchAppRequestType, fetchDataSourcePaginationRequestType, + fetchDBRequestType, + fetchFolderRequestType, + fetchGroupUserRequestType, fetchOrgsByEmailRequestType, + fetchOrgUserRequestType, fetchQueryLibraryPaginationRequestType, + orgGroupRequestType +} from "@lowcoder-ee/util/pagination/type"; +import OrgApi from "@lowcoder-ee/api/orgApi"; +import { DatasourceApi } from "@lowcoder-ee/api/datasourceApi"; +import {QueryLibraryApi} from "@lowcoder-ee/api/queryLibraryApi"; + +export const fetchFolderElements = async (request: fetchFolderRequestType) => { + try { + const response = await FolderApi.fetchFolderElementsPagination(request); + return { + success: true, + data: response.data.data, + total:response.data.total + }; + } catch (error) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchApplicationElements = async (request: fetchAppRequestType)=> { + try { + const response = await ApplicationApi.fetchAllApplicationsPagination(request); + return { + success: true, + data: response.data.data, + total: response.data.total + } + } catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchOrgGroups = async (request: orgGroupRequestType) => { + try{ + const response = await OrgApi.fetchGroupPagination(request); + return { + success: true, + data:response.data.data, + total:response.data.total + } + } + catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchDatasourcePagination = async (request: fetchDBRequestType)=> { + try { + const response = await DatasourceApi.fetchDatasourcePaginationByOrg(request); + return { + success: true, + data: response.data.data, + total: response.data.total + } + } catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchGroupUsrPagination = async (request: fetchGroupUserRequestType)=> { + try { + const response = await OrgApi.fetchGroupUsersPagination(request); + return { + success: true, + data: response.data.data.members, + total: response.data.data.total, + visitorRole: response.data.data.visitorRole + } + } catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchOrgUsrPagination = async (request: fetchOrgUserRequestType)=> { + try { + const response = await OrgApi.fetchOrgUsersPagination(request); + return { + success: true, + data: response.data.data.members, + total: response.data.data.total, + } + } catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchQLPaginationByOrg = async (request: fetchQueryLibraryPaginationRequestType)=> { + try { + const response = await QueryLibraryApi.fetchQueryLibraryPaginationByOrg(request); + return { + success: true, + data: response.data.data, + total: response.data.total + } + } catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchJsDSPaginationByApp = async (request: fetchDataSourcePaginationRequestType)=> { + try { + const response = await DatasourceApi.fetchJsDatasourcePaginationByApp(request); + return { + success: true, + data: response.data.data, + total: response.data.total + } + } catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + + + +export const fetchOrgPaginationByEmail = async (request: fetchOrgsByEmailRequestType)=> { + try { + const response = await OrgApi.fetchOrgsPaginationByEmail(request); + return { + success: true, + data: response.data.data, + total: response.data.total + } + } catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/util/pagination/type.ts b/client/packages/lowcoder/src/util/pagination/type.ts new file mode 100644 index 0000000000..f16bfcb808 --- /dev/null +++ b/client/packages/lowcoder/src/util/pagination/type.ts @@ -0,0 +1,108 @@ +import {GroupUser, OrgUser} from "@lowcoder-ee/constants/orgConstants"; + +type ApplicationType = { + [key: number]: string; // This allows numeric indexing +}; + +export const ApplicationPaginationType: ApplicationType = { + 0: "", + 1: "APPLICATION", + 2: "MODULE", + 3: "NAVLAYOUT", + 4: "FOLDER", + 6: "MOBILETABLAYOUT", + 7: "COMPOUND_APPLICATION", +}; + +export interface GenericApiPaginationResponse { + total: number; + success: boolean; + code: number; + message: string; + data: T; +} +export interface GroupUsersPaginationResponse { + success: boolean; + data: { + members: GroupUser[]; + visitorRole: string; + total: number; + }; +} + +export interface OrgUsersPaginationResponse { + success: boolean; + data: { + total: number; + members: OrgUser[]; + visitorRole: string; + }; +} + +export type ApiPaginationResponse = { + total: number; + success: boolean; + code: number; + message: string; + data: any; +}; + + +export interface fetchAppRequestType { + pageNum?: number; + pageSize?: number; + name?: string; + applicationType?: number; +} + +export interface fetchFolderRequestType { + id?: string; + pageNum?: number; + pageSize?: number; + name?: string; + applicationType?: string; + category?: string +} + +export interface fetchDBRequestType { + orgId: string; + pageNum?: number; + pageSize?: number; + name?: string; + type?: string; +} + +export interface orgGroupRequestType{ + pageNum?: number; + pageSize?: number; +} +export interface fetchOrgUserRequestType { + orgId: string; + pageNum?: number; + pageSize?: number; +} + +export interface fetchGroupUserRequestType { + groupId: string; + pageNum?: number; + pageSize?: number; +} + +export interface fetchQueryLibraryPaginationRequestType { + name?: string; + pageNum?: number; + pageSize?: number; +} + +export interface fetchDataSourcePaginationRequestType { + appId: string; + name?: string; + pageNum?: number; + pageSize?: number; +} + +export interface fetchOrgsByEmailRequestType { + email: string; + pageNum?: number; + pageSize?: number; +} \ No newline at end of file diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 2c2bc5c27d..5ecbbd579d 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -17,6 +17,7 @@ COPY server/api-service/lowcoder-server/src/main/resources/application.yaml /low # Add bootstrapfile COPY deploy/docker/api-service/entrypoint.sh /lowcoder/api-service/entrypoint.sh COPY deploy/docker/api-service/init.sh /lowcoder/api-service/init.sh +ENV JAVA_OPTS="-Xmx2G -Xms512M" RUN chmod +x /lowcoder/api-service/*.sh ## diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java index cce006b663..57ad9d720c 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java @@ -179,6 +179,18 @@ public Map getEditingApplicationDSL() { return dsl; } + public String getCategory() { + if(editingApplicationDSL == null || editingApplicationDSL.get("settings") == null) return ""; + Object settingsObject = editingApplicationDSL.get("settings"); + if (settingsObject instanceof Map) { + @SuppressWarnings("unchecked") + Map settings = (Map) editingApplicationDSL.get("settings"); + return (String) settings.get("category"); + } else { + return ""; + } + } + public Map getEditingApplicationDSLOrNull() {return editingApplicationDSL; } public Object getLiveContainerSize() { diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationType.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationType.java index bdda2ed115..9a953cc3f8 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationType.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationType.java @@ -8,7 +8,8 @@ public enum ApplicationType { APPLICATION(1), MODULE(2), - COMPOUND_APPLICATION(3); + COMPOUND_APPLICATION(3), + FOLDER(4); private final int value; diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistoryArchivedSnapshotRepository.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistoryArchivedSnapshotRepository.java index dded29c35e..548d6e439b 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistoryArchivedSnapshotRepository.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistoryArchivedSnapshotRepository.java @@ -1,6 +1,6 @@ package org.lowcoder.domain.application.repository; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; @@ -11,7 +11,7 @@ import java.time.Instant; @Repository -public interface ApplicationHistoryArchivedSnapshotRepository extends ReactiveMongoRepository { +public interface ApplicationHistoryArchivedSnapshotRepository extends ReactiveMongoRepository { @Query(value = "{ 'applicationId': ?0, $and: [" + "{$or: [ { 'context.operations': { $elemMatch: { 'compName': ?1 } } }, { $expr: { $eq: [?1, null] } } ]}, " + @@ -20,7 +20,7 @@ public interface ApplicationHistoryArchivedSnapshotRepository extends ReactiveMo "{$or: [ { 'createdAt': { $lte: ?4} }, { $expr: { $eq: [?4, null] } } ] } " + "]}", fields = "{applicationId : 1, context: 1, createdBy : 1, createdAt : 1}") - Flux findAllByApplicationId(String applicationId, String compName, String theme, Instant createdAtFrom, Instant createdAtTo, Pageable pageable); + Flux findAllByApplicationId(String applicationId, String compName, String theme, Instant createdAtFrom, Instant createdAtTo, Pageable pageable); Mono countByApplicationId(String applicationId); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java index 809decfd60..eabf2caf6f 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java @@ -1,6 +1,6 @@ package org.lowcoder.domain.application.repository; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; @@ -11,7 +11,7 @@ import java.time.Instant; @Repository -public interface ApplicationHistorySnapshotRepository extends ReactiveMongoRepository { +public interface ApplicationHistorySnapshotRepository extends ReactiveMongoRepository { @Query(value = "{ 'applicationId': ?0, $and: [" + "{$or: [ { 'context.operations': { $elemMatch: { 'compName': ?1 } } }, { $expr: { $eq: [?1, null] } } ]}, " + @@ -20,7 +20,7 @@ public interface ApplicationHistorySnapshotRepository extends ReactiveMongoRepos "{$or: [ { 'createdAt': { $lte: ?4} }, { $expr: { $eq: [?4, null] } } ] } " + "]}", fields = "{applicationId : 1, context: 1, createdBy : 1, createdAt : 1}") - Flux findAllByApplicationId(String applicationId, String compName, String theme, Instant createdAtFrom, Instant createdAtTo, Pageable pageable); + Flux findAllByApplicationId(String applicationId, String compName, String theme, Instant createdAtFrom, Instant createdAtTo, Pageable pageable); Mono countByApplicationId(String applicationId); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationRepository.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationRepository.java index 36f6fc96bb..9197d6bb4a 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationRepository.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationRepository.java @@ -4,6 +4,7 @@ import jakarta.annotation.Nonnull; import org.lowcoder.domain.application.model.Application; import org.lowcoder.domain.application.model.ApplicationStatus; +import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.stereotype.Repository; @@ -16,16 +17,16 @@ public interface ApplicationRepository extends ReactiveMongoRepository, CustomApplicationRepository { // publishedApplicationDSL : 0 -> excludes publishedApplicationDSL from the return - @Query(fields = "{ publishedApplicationDSL : 0 , editingApplicationDSL : 0 }") + @Aggregation(pipeline = {"{ $match: { organizationId: ?0 } }", "{ $project: { 'editingApplicationDSL.settings.category': 1, _id: 1, gid: 1, organizationId: 1, name: 1, applicationType: 1, applicationStatus: 1, publicToAll: 1, publicToMarketplace: 1, agencyProfile: 1, editingUserId: 1, lastEditedAt: 1, createdAt: 1, updatedAt: 1, createdBy: 1, modifiedBy: 1, _class: 1}}"}) Flux findByOrganizationId(String organizationId); @Override @Nonnull - @Query(fields = "{ publishedApplicationDSL : 0 , editingApplicationDSL : 0 }") + @Aggregation(pipeline = {"{ $match: { _id: ?0 } }", "{ $project: { 'editingApplicationDSL.settings.category': 1, _id: 1, gid: 1, organizationId: 1, name: 1, applicationType: 1, applicationStatus: 1, publicToAll: 1, publicToMarketplace: 1, agencyProfile: 1, editingUserId: 1, lastEditedAt: 1, createdAt: 1, updatedAt: 1, createdBy: 1, modifiedBy: 1, _class: 1}}"}) Mono findById(@Nonnull String id); - @Query(fields = "{ publishedApplicationDSL : 0 , editingApplicationDSL : 0 }") + @Aggregation(pipeline = {"{ $match: { gid: ?0 } }", "{ $project: { 'editingApplicationDSL.settings.category': 1, _id: 1, gid: 1, organizationId: 1, name: 1, applicationType: 1, applicationStatus: 1, publicToAll: 1, publicToMarketplace: 1, agencyProfile: 1, editingUserId: 1, lastEditedAt: 1, createdAt: 1, updatedAt: 1, createdBy: 1, modifiedBy: 1, _class: 1}}"}) Flux findByGid(@Nonnull String gid); Mono countByOrganizationIdAndApplicationStatus(String organizationId, ApplicationStatus applicationStatus); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java index fd4a79f824..f4e5b3fcf6 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java @@ -13,12 +13,13 @@ public interface ApplicationHistorySnapshotService { Mono createHistorySnapshot(String applicationId, Map dsl, Map context, String userId); - Mono> listAllHistorySnapshotBriefInfo(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest); - Mono> listAllHistorySnapshotBriefInfoArchived(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest); + Mono> listAllHistorySnapshotBriefInfo(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest); + Mono> listAllHistorySnapshotBriefInfoArchived(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest); Mono countByApplicationId(String applicationId); + Mono countByApplicationIdArchived(String applicationId); - Mono getHistorySnapshotDetail(String historySnapshotId); + Mono getHistorySnapshotDetail(String historySnapshotId); - Mono getHistorySnapshotDetailArchived(String historySnapshotId); + Mono getHistorySnapshotDetailArchived(String historySnapshotId); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java index c47b399556..2d4aba44a4 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java @@ -29,24 +29,24 @@ public class ApplicationHistorySnapshotServiceImpl implements ApplicationHistory @Override public Mono createHistorySnapshot(String applicationId, Map dsl, Map context, String userId) { - ApplicationHistorySnapshotTS applicationHistorySnapshotTS = new ApplicationHistorySnapshotTS(); - applicationHistorySnapshotTS.setApplicationId(applicationId); - applicationHistorySnapshotTS.setDsl(dsl); - applicationHistorySnapshotTS.setContext(context); - return repository.save(applicationHistorySnapshotTS) + ApplicationHistorySnapshot applicationHistorySnapshot = new ApplicationHistorySnapshot(); + applicationHistorySnapshot.setApplicationId(applicationId); + applicationHistorySnapshot.setDsl(dsl); + applicationHistorySnapshot.setContext(context); + return repository.save(applicationHistorySnapshot) .thenReturn(true) .onErrorReturn(false); } @Override - public Mono> listAllHistorySnapshotBriefInfo(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest) { + public Mono> listAllHistorySnapshotBriefInfo(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest) { return repository.findAllByApplicationId(applicationId, compName, theme, from, to, pageRequest.withSort(Direction.DESC, "id")) .collectList() .onErrorMap(Exception.class, e -> ofException(BizError.FETCH_HISTORY_SNAPSHOT_FAILURE, "FETCH_HISTORY_SNAPSHOT_FAILURE")); } @Override - public Mono> listAllHistorySnapshotBriefInfoArchived(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest) { + public Mono> listAllHistorySnapshotBriefInfoArchived(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest) { return repositoryArchived.findAllByApplicationId(applicationId, compName, theme, from, to, pageRequest.withSort(Direction.DESC, "id")) .collectList() .onErrorMap(Exception.class, e -> ofException(BizError.FETCH_HISTORY_SNAPSHOT_FAILURE, "FETCH_HISTORY_SNAPSHOT_FAILURE")); @@ -59,16 +59,23 @@ public Mono countByApplicationId(String applicationId) { e -> ofException(BizError.FETCH_HISTORY_SNAPSHOT_COUNT_FAILURE, "FETCH_HISTORY_SNAPSHOT_COUNT_FAILURE")); } + @Override + public Mono countByApplicationIdArchived(String applicationId) { + return repositoryArchived.countByApplicationId(applicationId) + .onErrorMap(Exception.class, + e -> ofException(BizError.FETCH_HISTORY_SNAPSHOT_COUNT_FAILURE, "FETCH_HISTORY_SNAPSHOT_COUNT_FAILURE")); + } + @Override - public Mono getHistorySnapshotDetail(String historySnapshotId) { + public Mono getHistorySnapshotDetail(String historySnapshotId) { return repository.findById(historySnapshotId) .switchIfEmpty(deferredError(INVALID_HISTORY_SNAPSHOT, "INVALID_HISTORY_SNAPSHOT", historySnapshotId)); } @Override - public Mono getHistorySnapshotDetailArchived(String historySnapshotId) { + public Mono getHistorySnapshotDetailArchived(String historySnapshotId) { return repositoryArchived.findById(historySnapshotId) .switchIfEmpty(deferredError(INVALID_HISTORY_SNAPSHOT, "INVALID_HISTORY_SNAPSHOT", historySnapshotId)); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrgMemberServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrgMemberServiceImpl.java index 65c6a8945e..fdf6127e64 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrgMemberServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrgMemberServiceImpl.java @@ -53,7 +53,7 @@ public Flux getOrganizationMembers(String orgId) { @Override public Flux getOrganizationMembers(String orgId, int page, int count) { - return biRelationService.getBySourceId(ORG_MEMBER, orgId, PageRequest.of(page, count)) + return biRelationService.getBySourceId(ORG_MEMBER, orgId, PageRequest.of(page - 1, count)) .map(OrgMember::from); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java index 9a2bb24cc5..a1358b39f8 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java @@ -91,9 +91,6 @@ public Mono createDefault(User user, boolean isSuperAdmin) { if (Boolean.TRUE.equals(join)) { return Mono.empty(); } - OrganizationDomain organizationDomain = new OrganizationDomain(); - organizationDomain.setConfigs(List.of(DEFAULT_AUTH_CONFIG)); - organization.setOrganizationDomain(organizationDomain); return create(organization, user.getId(), isSuperAdmin); }); }); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index 6b800720cd..981000caf5 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -418,7 +418,7 @@ protected Map convertConnections(Set connections) { return connections.stream() .filter(connection -> !AuthSourceConstants.EMAIL.equals(connection.getSource()) && !AuthSourceConstants.PHONE.equals(connection.getSource())) - .collect(Collectors.toMap(Connection::getSource, Connection::getRawUserInfo)); + .collect(Collectors.toMap(Connection::getAuthId, Connection::getRawUserInfo)); } protected String convertEmail(Set connections) { diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/constant/NewUrl.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/constant/NewUrl.java index 30eed2e579..01eb05ccff 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/constant/NewUrl.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/constant/NewUrl.java @@ -34,4 +34,6 @@ private NewUrl() { public static final String MATERIAL_URL = PREFIX + "/materials"; public static final String CONTACT_SYNC = PREFIX + "/sync"; public static final String NPM_REGISTRY = PREFIX + "/npm"; + + public static final String PLUGINS_URL = PREFIX + "/plugins"; } diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java index eab8706056..fa280173dd 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/exception/BizError.java @@ -11,7 +11,7 @@ public enum BizError { // 5000 - 5100 general errorCode INTERNAL_SERVER_ERROR(500, 5000, VERBOSE), - NOT_AUTHORIZED(500, 5001), + NOT_AUTHORIZED(401, 5001), INVALID_PARAMETER(500, 5002), UNSUPPORTED_OPERATION(400, 5003), DUPLICATE_KEY(409, 5004, VERBOSE), @@ -113,6 +113,7 @@ public enum BizError { PLUGIN_EXECUTION_TIMEOUT(504, 5800), INVALID_DATASOURCE_TYPE(500, 5801), PLUGIN_EXECUTION_TIMEOUT_WITHOUT_TIME(504, 5802, VERBOSE), + PLUGIN_ENDPOINT_ERROR(500, 5850), // business related, code range 5900 - 5999 NOT_RELEASE(423, 5901), diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiService.java index 88d14e210e..e0990e1348 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiService.java @@ -17,7 +17,7 @@ public interface ApplicationApiService { Mono create(ApplicationEndpoints.CreateApplicationRequest createApplicationRequest); - Flux getRecycledApplications(String name); + Flux getRecycledApplications(String name, String category); Mono delete(String applicationId); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java index 25d772cdb3..e7ae4e0dda 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java @@ -169,8 +169,8 @@ private Mono autoGrantPermissionsByFolderDefault(String applicationId, @Nu } @Override - public Flux getRecycledApplications(String name) { - return userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(null, ApplicationStatus.RECYCLED, false, name); + public Flux getRecycledApplications(String name, String category) { + return userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(null, ApplicationStatus.RECYCLED, false, name, category); } private Mono checkCurrentUserApplicationPermission(String applicationId, ResourceAction action) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java index ed7079598c..1fe9788e22 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java @@ -78,8 +78,8 @@ public Mono> restore(@PathVariable String applicationId) { } @Override - public Mono>> getRecycledApplications(@RequestParam(required = false) String name) { - return applicationApiService.getRecycledApplications(name) + public Mono>> getRecycledApplications(@RequestParam(required = false) String name, @RequestParam(required = false) String category) { + return applicationApiService.getRecycledApplications(name, category) .collectList() .map(ResponseView::success); } @@ -159,15 +159,16 @@ public Mono> getUserHomePage(@RequestParam(requir @Override public Mono>> getApplications(@RequestParam(required = false) Integer applicationType, - @RequestParam(required = false) ApplicationStatus applicationStatus, - @RequestParam(defaultValue = "true") boolean withContainerSize, - @RequestParam(required = false) String name, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, - @RequestParam(required = false, defaultValue = "0") Integer pageSize) { + @RequestParam(required = false) ApplicationStatus applicationStatus, + @RequestParam(defaultValue = "true") boolean withContainerSize, + @RequestParam(required = false) String name, + @RequestParam(required = false) String category, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @RequestParam(required = false, defaultValue = "0") Integer pageSize) { ApplicationType applicationTypeEnum = applicationType == null ? null : ApplicationType.fromValue(applicationType); - var flux = userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(applicationTypeEnum, applicationStatus, withContainerSize, name).cache(); + var flux = userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(applicationTypeEnum, applicationStatus, withContainerSize, name, category).cache(); Mono countMono = flux.count(); - var flux1 = flux.skip((long) pageNum * pageSize); + var flux1 = flux.skip((long) (pageNum - 1) * pageSize); if(pageSize > 0) flux1 = flux1.take(pageSize); return flux1.collectList().zipWith(countMono) .map(tuple -> PageResponseView.success(tuple.getT1(), pageNum, pageSize, Math.toIntExact(tuple.getT2()))); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java index 4eed69ee2c..78121eec47 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java @@ -71,7 +71,7 @@ public interface ApplicationEndpoints description = "List all the recycled Lowcoder Applications in the recycle bin where the authenticated or impersonated user has access." ) @GetMapping("/recycle/list") - public Mono>> getRecycledApplications(@RequestParam(required = false) String name); + public Mono>> getRecycledApplications(@RequestParam(required = false) String name, @RequestParam(required = false) String category); @Operation( tags = TAG_APPLICATION_MANAGEMENT, @@ -167,7 +167,8 @@ public Mono>> getApplications(@RequestPar @RequestParam(required = false) ApplicationStatus applicationStatus, @RequestParam(defaultValue = "true") boolean withContainerSize, @RequestParam(required = false) String name, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false) String category, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java index 6b6d94a513..b5a6381d70 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java @@ -13,6 +13,7 @@ import org.lowcoder.domain.application.service.ApplicationService; import org.lowcoder.domain.permission.model.ResourceAction; import org.lowcoder.domain.permission.service.ResourcePermissionService; +import org.lowcoder.domain.user.model.User; import org.lowcoder.domain.user.service.UserService; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -54,7 +55,7 @@ public Mono> create(@RequestBody ApplicationHistorySnapsho @Override public Mono>> listAllHistorySnapshotBriefInfo(@PathVariable String applicationId, - @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, @RequestParam String compName, @RequestParam String theme, @@ -69,15 +70,15 @@ public Mono>> listAllHistorySnapshotBriefInfo(@ .flatMap(__ -> applicationHistorySnapshotService.listAllHistorySnapshotBriefInfo(applicationId, compName, theme, from, to, pagination.toPageRequest())) .flatMap(snapshotList -> { Mono> snapshotBriefInfoList = multiBuild(snapshotList, - ApplicationHistorySnapshotTS::getCreatedBy, + ApplicationHistorySnapshot::getCreatedBy, userService::getByIds, - (applicationHistorySnapshotTS, user) -> new ApplicationHistorySnapshotBriefInfo( - applicationHistorySnapshotTS.getId(), - applicationHistorySnapshotTS.getContext(), - applicationHistorySnapshotTS.getCreatedBy(), + (applicationHistorySnapshot, user) -> new ApplicationHistorySnapshotBriefInfo( + applicationHistorySnapshot.getId(), + applicationHistorySnapshot.getContext(), + applicationHistorySnapshot.getCreatedBy(), user.getName(), user.getAvatarUrl(), - applicationHistorySnapshotTS.getCreatedAt().toEpochMilli() + applicationHistorySnapshot.getCreatedAt().toEpochMilli() ) ); @@ -91,7 +92,7 @@ public Mono>> listAllHistorySnapshotBriefInfo(@ @Override public Mono>> listAllHistorySnapshotBriefInfoArchived(@PathVariable String applicationId, - @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, @RequestParam String compName, @RequestParam String theme, @@ -106,19 +107,19 @@ public Mono>> listAllHistorySnapshotBriefInfoAr .flatMap(__ -> applicationHistorySnapshotService.listAllHistorySnapshotBriefInfoArchived(applicationId, compName, theme, from, to, pagination.toPageRequest())) .flatMap(snapshotList -> { Mono> snapshotBriefInfoList = multiBuild(snapshotList, - ApplicationHistorySnapshot::getCreatedBy, + ApplicationHistorySnapshotTS::getCreatedBy, userService::getByIds, - (applicationHistorySnapshot, user) -> new ApplicationHistorySnapshotBriefInfo( - applicationHistorySnapshot.getId(), - applicationHistorySnapshot.getContext(), - applicationHistorySnapshot.getCreatedBy(), + (applicationHistorySnapshotTS, user) -> new ApplicationHistorySnapshotBriefInfo( + applicationHistorySnapshotTS.getId(), + applicationHistorySnapshotTS.getContext(), + applicationHistorySnapshotTS.getCreatedBy(), user.getName(), user.getAvatarUrl(), - applicationHistorySnapshot.getCreatedAt().toEpochMilli() + applicationHistorySnapshotTS.getCreatedAt().toEpochMilli() ) ); - Mono applicationHistorySnapshotCount = applicationHistorySnapshotService.countByApplicationId(applicationId); + Mono applicationHistorySnapshotCount = applicationHistorySnapshotService.countByApplicationIdArchived(applicationId); return Mono.zip(snapshotBriefInfoList, applicationHistorySnapshotCount) .map(tuple -> ImmutableMap.of("list", tuple.getT1(), "count", tuple.getT2())); @@ -133,7 +134,7 @@ public Mono> getHistorySnapshotDsl(@PathVar .delayUntil(visitor -> resourcePermissionService.checkResourcePermissionWithError(visitor, applicationId, ResourceAction.EDIT_APPLICATIONS)) .flatMap(__ -> applicationHistorySnapshotService.getHistorySnapshotDetail(snapshotId)) - .map(ApplicationHistorySnapshotTS::getDsl) + .map(ApplicationHistorySnapshot::getDsl) .zipWhen(applicationService::getAllDependentModulesFromDsl) .map(tuple -> { Map applicationDsl = tuple.getT1(); @@ -155,7 +156,7 @@ public Mono> getHistorySnapshotDslArchived( .delayUntil(visitor -> resourcePermissionService.checkResourcePermissionWithError(visitor, applicationId, ResourceAction.EDIT_APPLICATIONS)) .flatMap(__ -> applicationHistorySnapshotService.getHistorySnapshotDetailArchived(snapshotId)) - .map(ApplicationHistorySnapshot::getDsl) + .map(ApplicationHistorySnapshotTS::getDsl) .zipWhen(applicationService::getAllDependentModulesFromDsl) .map(tuple -> { Map applicationDsl = tuple.getT1(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java index b4e3e2c4dc..df1c9e1d19 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java @@ -228,7 +228,7 @@ protected Connection getAuthConnection(AuthUser authUser, User user) { return user.getConnections() .stream() .filter(connection -> authUser.getSource().equals(connection.getSource()) - && connection.getRawId().equals(authUser.getUid())) + && Objects.equals(connection.getRawId(), authUser.getUid())) .findFirst() .get(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleController.java index 254e780373..cb0df92418 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleController.java @@ -106,7 +106,7 @@ public Mono>> getRecycledBundles() { @Override public Mono> getElements(@PathVariable String bundleId, @RequestParam(value = "applicationType", required = false) ApplicationType applicationType, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize) { String objectId = gidService.convertBundleIdToObjectId(bundleId); var flux = bundleApiService.getElements(objectId, applicationType).cache(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleEndpoints.java index 8674c62b5f..8d668c1b75 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleEndpoints.java @@ -123,7 +123,7 @@ public interface BundleEndpoints @GetMapping("/{bundleId}/elements") public Mono> getElements(@PathVariable String bundleId, @RequestParam(value = "applicationType", required = false) ApplicationType applicationType, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java index 4d00716390..695245c415 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java @@ -119,7 +119,7 @@ public Mono> getStructure(@PathVariable String */ @Override public Mono> listJsDatasourcePlugins(@RequestParam("appId") String applicationId, @RequestParam(required = false) String name, @RequestParam(required = false) String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int pageSize) { String objectId = gidService.convertApplicationIdToObjectId(applicationId); return fluxToPageResponseView(pageNum, pageSize, datasourceApiService.listJsDatasourcePlugins(objectId, name, type)); @@ -142,7 +142,7 @@ public Mono>> getPluginDynamicConfig( @SneakyThrows @Override public Mono> listOrgDataSources(@RequestParam(name = "orgId") String orgId, @RequestParam(required = false) String name, @RequestParam(required = false) String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int pageSize) { if (StringUtils.isBlank(orgId)) { return ofError(BizError.INVALID_PARAMETER, "ORG_ID_EMPTY"); @@ -153,7 +153,7 @@ public Mono> listOrgDataSources(@RequestParam(name = "orgId" @Override public Mono> listAppDataSources(@RequestParam(name = "appId") String applicationId, @RequestParam(required = false) String name, @RequestParam(required = false) String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int pageSize) { if (StringUtils.isBlank(applicationId)) { return ofError(BizError.INVALID_PARAMETER, "INVALID_APP_ID"); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java index d3608533df..775d702290 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java @@ -101,7 +101,7 @@ public Mono> getStructure(@PathVariable String ) @GetMapping("/jsDatasourcePlugins") public Mono> listJsDatasourcePlugins(@RequestParam("appId") String applicationId, @RequestParam(required = false) String name, @RequestParam(required = false) String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int pageSize); /** @@ -127,7 +127,7 @@ public Mono>> getPluginDynamicConfig( @JsonView(JsonViews.Public.class) @GetMapping("/listByOrg") public Mono> listOrgDataSources(@RequestParam(name = "orgId") String orgId, @RequestParam String name, @RequestParam String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int pageSize); @Operation( @@ -140,7 +140,7 @@ public Mono> listOrgDataSources(@RequestParam(name = "orgId" @JsonView(JsonViews.Public.class) @GetMapping("/listByApp") public Mono> listAppDataSources(@RequestParam(name = "appId") String applicationId, @RequestParam String name, @RequestParam String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/exception/GlobalExceptionHandler.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/exception/GlobalExceptionHandler.java index edd37f469e..5b6579e084 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/exception/GlobalExceptionHandler.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/exception/GlobalExceptionHandler.java @@ -8,7 +8,9 @@ import java.util.Map; import java.util.concurrent.TimeoutException; +import org.apache.commons.lang3.StringUtils; import org.lowcoder.api.framework.view.ResponseView; +import org.lowcoder.infra.constant.NewUrl; import org.lowcoder.infra.util.LogUtils; import org.lowcoder.sdk.exception.BaseException; import org.lowcoder.sdk.exception.BizError; @@ -26,6 +28,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.support.WebExchangeBindException; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; @@ -133,6 +136,23 @@ public Mono> catchServerException(ServerException e, ServerWebEx }); } + @ExceptionHandler + @ResponseBody + public Mono> catchResponseStatusException(ResponseStatusException e, ServerWebExchange exchange) { + if (StringUtils.startsWith(exchange.getRequest().getPath().toString(), NewUrl.PLUGINS_URL + "/")) { + BizError bizError = BizError.PLUGIN_ENDPOINT_ERROR; + exchange.getResponse().setStatusCode(e.getStatusCode()); + return Mono.deferContextual(ctx -> { + apiPerfHelper.perf(bizError, exchange.getRequest().getPath()); + doLog(e, ctx, bizError.logVerbose()); + return Mono.just(error(bizError.getBizErrorCode(), e.getMessage() + " - path: " + exchange.getRequest().getPath())); + }); + + } else { + return catchException(e, exchange); + } + } + @ExceptionHandler @ResponseBody public Mono> catchException(java.lang.Exception e, ServerWebExchange exchange) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java index 1e1b3c8e38..eeaf1d9117 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java @@ -132,7 +132,10 @@ public Mono runPluginEndpointMethod(PluginEndpoint endpoint, End }); return decisionMono.handle((authorizationDecision, sink) -> { - if(!authorizationDecision.isGranted()) sink.error(new BizException(NOT_AUTHORIZED, "NOT_AUTHORIZED")); + if(!authorizationDecision.isGranted()) { + sink.error(new BizException(NOT_AUTHORIZED, "NOT_AUTHORIZED")); + return; + } try { sink.next((EndpointResponse) handler.invoke(endpoint, PluginServerRequest.fromServerRequest(request))); } catch (IllegalAccessException | InvocationTargetException e) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java index 551d851575..71b75c3e08 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java @@ -31,11 +31,12 @@ public PluginAuthorizationManager() public Mono check(Mono authentication, MethodInvocation invocation) { log.info("Checking plugin reactive endpoint invocation security for {}", invocation.getMethod().getName()); - + EndpointExtension endpointExtension = (EndpointExtension)invocation.getArguments()[1]; if (endpointExtension == null || StringUtils.isBlank(endpointExtension.authorize())) { - return Mono.empty(); + log.debug("Authorization expression is empty, proceeding without authorization - authorization granted."); + return Mono.just(new AuthorizationDecision(true)); } Expression authorizeExpression = this.expressionHandler.getExpressionParser() diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java index de161bb19e..81678ea787 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java @@ -27,7 +27,7 @@ public interface FolderApiService { Mono upsertLastViewTime(@Nullable String folderId); - Flux getElements(@Nullable String folderId, @Nullable ApplicationType applicationType, @Nullable String name); + Flux getElements(@Nullable String folderId, @Nullable ApplicationType applicationType, @Nullable String name, @Nullable String category); Mono grantPermission(String folderId, Set userIds, Set groupIds, ResourceRole role); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiServiceImpl.java index 79fafda96f..0d93a2e3b1 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiServiceImpl.java @@ -233,8 +233,8 @@ public Mono upsertLastViewTime(@Nullable String folderId) { * @return flux of {@link ApplicationInfoView} or {@link FolderInfoView} */ @Override - public Flux getElements(@Nullable String folderId, @Nullable ApplicationType applicationType, @Nullable String name) { - return buildApplicationInfoViewTree(applicationType, name) + public Flux getElements(@Nullable String folderId, @Nullable ApplicationType applicationType, @Nullable String name, @Nullable String category) { + return buildApplicationInfoViewTree(applicationType, name, category) .flatMap(tree -> { FolderNode folderNode = tree.get(folderId); if (folderNode == null) { @@ -278,13 +278,13 @@ private Mono> buildFolderTree(String orgId) { .map(folders -> new Tree<>(folders, Folder::getId, Folder::getParentFolderId, Collections.emptyList(), null, null)); } - private Mono> buildApplicationInfoViewTree(@Nullable ApplicationType applicationType, @Nullable String name) { + private Mono> buildApplicationInfoViewTree(@Nullable ApplicationType applicationType, @Nullable String name, @Nullable String category) { Mono orgMemberMono = sessionUserService.getVisitorOrgMemberCache() .cache(); Flux applicationInfoViewFlux = - userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(applicationType, ApplicationStatus.NORMAL, false, null) + userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(applicationType, ApplicationStatus.NORMAL, false, name, category) .cache(); Mono> application2FolderMapMono = applicationInfoViewFlux diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java index 31cf494948..1eb5415671 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java @@ -70,12 +70,13 @@ public Mono> update(@RequestBody Folder folder) { public Mono> getElements(@RequestParam(value = "id", required = false) String folderId, @RequestParam(value = "applicationType", required = false) ApplicationType applicationType, @RequestParam(required = false) String name, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false) String category, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize) { String objectId = gidService.convertFolderIdToObjectId(folderId); - var flux = folderApiService.getElements(objectId, applicationType, name).cache(); + var flux = folderApiService.getElements(objectId, applicationType, name, category).cache(); var countMono = flux.count(); - var flux1 = flux.skip((long) pageNum * pageSize); + var flux1 = flux.skip((long) (pageNum - 1) * pageSize); if(pageSize > 0) flux1 = flux1.take(pageSize); return flux1.collectList() .delayUntil(__ -> folderApiService.upsertLastViewTime(objectId)) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java index 43e5ce785f..3e3bdb083d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java @@ -71,7 +71,8 @@ public interface FolderEndpoints public Mono> getElements(@RequestParam(value = "id", required = false) String folderId, @RequestParam(value = "applicationType", required = false) ApplicationType applicationType, @RequestParam(required = false) String name, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false) String category, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiService.java index e711304a49..64aa092409 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiService.java @@ -24,7 +24,7 @@ public interface UserHomeApiService { Mono getUserHomePageView(ApplicationType applicationType); Flux getAllAuthorisedApplications4CurrentOrgMember(@Nullable ApplicationType applicationType, - @Nullable ApplicationStatus applicationStatus, boolean withContainerSize, @Nullable String name); + @Nullable ApplicationStatus applicationStatus, boolean withContainerSize, @Nullable String name, @Nullable String category); Flux getAllAuthorisedBundles4CurrentOrgMember(@Nullable BundleStatus bundleStatus); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java index ae5f22fcfc..421e451cc2 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java @@ -157,7 +157,7 @@ public Mono getUserHomePageView(ApplicationType applicationTyp } return organizationService.getById(currentOrgId) - .zipWith(folderApiService.getElements(null, applicationType, null).collectList()) + .zipWith(folderApiService.getElements(null, applicationType, null, null).collectList()) .map(tuple2 -> { Organization organization = tuple2.getT1(); List list = tuple2.getT2(); @@ -189,7 +189,7 @@ public Mono getUserHomePageView(ApplicationType applicationTyp @Override public Flux getAllAuthorisedApplications4CurrentOrgMember(@Nullable ApplicationType applicationType, - @Nullable ApplicationStatus applicationStatus, boolean withContainerSize, @Nullable String name) { + @Nullable ApplicationStatus applicationStatus, boolean withContainerSize, @Nullable String name, @Nullable String category) { return sessionUserService.getVisitorOrgMemberCache() .flatMapMany(orgMember -> { @@ -204,7 +204,8 @@ public Flux getAllAuthorisedApplications4CurrentOrgMember(@ }) .filter(application -> (isNull(applicationType) || application.getApplicationType() == applicationType.getValue()) && (isNull(applicationStatus) || application.getApplicationStatus() == applicationStatus) - && (isNull(name) || StringUtils.containsIgnoreCase(application.getName(), name))) + && (isNull(name) || StringUtils.containsIgnoreCase(application.getName(), name)) + && (isNull(category) || StringUtils.containsIgnoreCase(application.getCategory(), category))) .cache() .collectList() .flatMapIterable(Function.identity()); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/npm/PrivateNpmRegistryController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/npm/PrivateNpmRegistryController.java index 2f3614b846..9e967605c7 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/npm/PrivateNpmRegistryController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/npm/PrivateNpmRegistryController.java @@ -4,6 +4,8 @@ import org.jetbrains.annotations.NotNull; import org.lowcoder.api.home.SessionUserService; import org.lowcoder.domain.application.service.ApplicationServiceImpl; +import org.lowcoder.domain.organization.model.OrgMember; +import org.lowcoder.domain.organization.model.Organization; import org.lowcoder.domain.organization.service.OrgMemberServiceImpl; import org.lowcoder.domain.organization.service.OrganizationService; import org.lowcoder.infra.constant.NewUrl; @@ -49,23 +51,39 @@ private Mono> forwardToNodeService(String applicationId String withoutLeadingSlash = path.startsWith("/") ? path.substring(1) : path; if(applicationId.equals("none")) { - return sessionUserService.getVisitorOrgMemberCache().flatMap(orgMember -> organizationService.getOrgCommonSettings(orgMember.getOrgId()).flatMap(organizationCommonSettings -> { - Map config = Map.of("npmRegistries", Objects.requireNonNullElse(organizationCommonSettings.get("npmRegistries"), new ArrayList<>(0)), "workspaceId", orgMember.getOrgId()); - return WebClientBuildHelper.builder() - .systemProxy() - .build() - .post() - .uri(nodeServerHelper.createUri(prefix + "/" + withoutLeadingSlash)) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(config)) - .retrieve().toEntity(Resource.class) - .map(response -> { - return ResponseEntity - .status(response.getStatusCode()) - .headers(response.getHeaders()) - .body(response.getBody()); - }); - })); + return sessionUserService.getVisitorOrgMemberCache() + .onErrorResume(e -> Mono.just(OrgMember.builder().orgId("default").build())) + .flatMap(orgMember -> organizationService.getOrgCommonSettings(orgMember.getOrgId()) + .onErrorResume(e -> { + // Handle errors fetching organization settings and provide defaults + Organization.OrganizationCommonSettings defaultSettings = new Organization.OrganizationCommonSettings(); + defaultSettings.put("npmRegistries", new ArrayList<>(0)); + return Mono.just(defaultSettings); + }) + .flatMap(organizationCommonSettings -> { + Map config = Map.of( + "npmRegistries", Objects.requireNonNullElse( + organizationCommonSettings.get("npmRegistries"), + new ArrayList<>(0) + ), + "workspaceId", orgMember.getOrgId() + ); + return WebClientBuildHelper.builder() + .systemProxy() + .build() + .post() + .uri(nodeServerHelper.createUri(prefix + "/" + withoutLeadingSlash)) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(config)) + .retrieve() + .toEntity(Resource.class) + .map(response -> ResponseEntity + .status(response.getStatusCode()) + .headers(response.getHeaders()) + .body(response.getBody()) + ); + })); + } else{ return applicationServiceImpl.findById(applicationId).flatMap(application -> organizationService.getById(application.getOrganizationId())).flatMap(orgMember -> organizationService.getOrgCommonSettings(orgMember.getId()).flatMap(organizationCommonSettings -> { Map config = Map.of("npmRegistries", Objects.requireNonNullElse(organizationCommonSettings.get("npmRegistries"), new ArrayList<>(0)), "workspaceId", orgMember.getId()); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiServiceImpl.java index 572f7ccdd3..69c5c3f8fb 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiServiceImpl.java @@ -87,7 +87,10 @@ private Flux getByOrgIdWithDatasourcePermissions(String orgId) { Flux libraryQueryFlux = libraryQueryService.getByOrganizationId(orgId) .cache(); - Mono> datasourceIdListMono = libraryQueryFlux.map(libraryQuery -> libraryQuery.getQuery().getDatasourceId()) + Mono> datasourceIdListMono = libraryQueryFlux.map(libraryQuery -> { + var datasourceId = libraryQuery.getQuery().getDatasourceId(); + return Objects.requireNonNullElse(datasourceId, ""); + }) .filter(StringUtils::isNotBlank) .collectList() .cache(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java index be0e7de68e..a7a5a320d7 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java @@ -46,7 +46,7 @@ public Mono>> dropDownList(@Request @Override public Mono> list(@RequestParam(required = false, defaultValue = "") String name, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int pageSize) { var flux = libraryQueryApiService.listLibraryQueries(name) .flatMapMany(Flux::fromIterable); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java index c4acd37497..bf4b8f161b 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java @@ -40,7 +40,7 @@ public interface LibraryQueryEndpoints ) @GetMapping("/listByOrg") public Mono> list(@RequestParam(required = false, defaultValue = "") String name, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordController.java index 31a1b8b4dd..9db6a9ea26 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordController.java @@ -30,7 +30,7 @@ public Mono delete(@PathVariable String libraryQueryRecordId) { @Override public Mono> getByLibraryQueryId(@RequestParam(name = "libraryQueryId") String libraryQueryId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int pageSize) { return fluxToPageResponseView(pageNum, pageSize, libraryQueryRecordApiService.getByLibraryQueryId(libraryQueryId).flatMapMany(Flux::fromIterable)); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordEndpoints.java index 7fb642fb0f..9f41f380d2 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordEndpoints.java @@ -41,7 +41,7 @@ public interface LibraryQueryRecordEndpoints ) @GetMapping("/listByLibraryQueryId") public Mono> getByLibraryQueryId(@RequestParam(name = "libraryQueryId") String libraryQueryId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiServiceImpl.java index 07e97fc9da..1ae81589aa 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiServiceImpl.java @@ -98,7 +98,7 @@ public Mono getGroupMembers(String groupId, int page, .filter(Objects::nonNull) .toList(); var pageTotal = list.size(); - list = list.subList(page * count, Math.min(page * count + count, pageTotal)); + list = list.subList((page - 1) * count, count == 0 ? pageTotal : Math.min(page * count, pageTotal)); return Pair.of(list, pageTotal); }); }) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java index d478bcfc28..a7adcb6ecf 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java @@ -75,7 +75,7 @@ public Mono> delete(@PathVariable String groupId) { } @Override - public Mono>> getOrgGroups(@RequestParam(required = false, defaultValue = "0") Integer pageNum, + public Mono>> getOrgGroups(@RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize) { return groupApiService.getGroups().flatMap(groupList -> { if(groupList.isEmpty()) return Mono.just(new GroupListResponseView<>(ResponseView.SUCCESS, @@ -99,7 +99,7 @@ public Mono>> getOrgGroups(@RequestParam(r .filter(orgMember -> !orgMember.isAdmin() && !orgMember.isSuperAdmin() && devMembers.stream().noneMatch(devMember -> devMember.getUserId().equals(orgMember.getUserId()))).toList().size(); - var subList = groupList.subList(pageNum * pageSize, pageSize <= 0?groupList.size():pageNum * pageSize + pageSize); + var subList = groupList.subList((pageNum - 1) * pageSize, pageSize <= 0?groupList.size():Math.min(pageNum * pageSize, groupList.size())); return new GroupListResponseView<>(ResponseView.SUCCESS, "", subList, @@ -107,7 +107,7 @@ public Mono>> getOrgGroups(@RequestParam(r totalAdminsAndDevelopers, totalDevelopersOnly, totalOtherMembers, - subList.size(), + groupList.size(), pageNum, pageSize); }) @@ -119,7 +119,7 @@ public Mono>> getOrgGroups(@RequestParam(r @Override public Mono> getGroupMembers(@PathVariable String groupId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int pageSize) { String objectId = gidService.convertGroupIdToObjectId(groupId); return groupApiService.getGroupMembers(objectId, pageNum, pageSize) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java index 4f08253336..89e294628d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java @@ -63,7 +63,7 @@ public Mono> update(@PathVariable String groupId, description = "Retrieve a list of User Groups within Lowcoder, providing an overview of available groups, based on the access rights of the currently impersonated User." ) @GetMapping("/list") - public Mono>> getOrgGroups(@RequestParam(required = false, defaultValue = "0") Integer pageNum, + public Mono>> getOrgGroups(@RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize); @Operation( @@ -74,7 +74,7 @@ public Mono>> getOrgGroups(@RequestParam(r ) @GetMapping("/{groupId}/members") public Mono> getGroupMembers(@PathVariable String groupId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java index 0a68beb8a3..1b1036c24e 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java @@ -85,7 +85,7 @@ public Mono getOrganizationMembers(String orgId, int page, in } private Mono getOrgMemberListView(String orgId, int page, int count) { - return orgMemberService.getOrganizationMembers(orgId, page, count) + return orgMemberService.getOrganizationMembers(orgId) .collectList() .flatMap(orgMembers -> { List userIds = orgMembers.stream() @@ -106,7 +106,7 @@ private Mono getOrgMemberListView(String orgId, int page, int .filter(Objects::nonNull) .collect(Collectors.toList()); var pageTotal = list.size(); - list = list.subList(page * count, Math.min(page * count + count, pageTotal)); + list = list.subList((page - 1) * count, count == 0 ? pageTotal : Math.min(page * count, pageTotal)); return Pair.of(list, pageTotal); }); }) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java index b0acc8cf1a..d43676ba53 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java @@ -50,7 +50,7 @@ public class OrganizationController implements OrganizationEndpoints @Override public Mono> getOrganizationByUser(@PathVariable String email, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize) { var flux = userService.findByEmailDeep(email).flux().flatMap(user -> orgMemberService.getAllActiveOrgs(user.getId())) .flatMap(orgMember -> organizationService.getById(orgMember.getOrgId())) @@ -90,7 +90,7 @@ public Mono> deleteLogo(@PathVariable String orgId) { @Override public Mono> getOrgMembers(@PathVariable String orgId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "1000") int pageSize) { String id = gidService.convertOrganizationIdToObjectId(orgId); return orgApiService.getOrganizationMembers(id, pageNum, pageSize) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java index 7340120336..8fc9d55988 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java @@ -46,7 +46,7 @@ public interface OrganizationEndpoints ) @GetMapping("/byuser/{email}") public Mono> getOrganizationByUser(@PathVariable String email, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize); @Operation( @@ -95,7 +95,7 @@ public Mono> uploadLogo(@PathVariable String orgId, ) @GetMapping("/{orgId}/members") public Mono> getOrgMembers(@PathVariable String orgId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "1000") int pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/Pagination.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/Pagination.java index 051c3e006a..03141d6bb0 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/Pagination.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/Pagination.java @@ -64,7 +64,7 @@ public int size() { @NotNull public static Mono> fluxToPageResponseView(Integer pageNum, Integer pageSize, Flux flux) { var countMono = flux.count(); - var flux1 = flux.skip((long) pageNum * pageSize); + var flux1 = flux.skip((long) (pageNum - 1) * pageSize); if(pageSize > 0) flux1 = flux1.take(pageSize); return flux1.collectList().zipWith(countMono) .map(tuple -> PageResponseView.success(tuple.getT1(), pageNum, pageSize, Math.toIntExact(tuple.getT2()))); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java index ddf0422ab6..a51a74e09a 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java @@ -4,6 +4,9 @@ import com.github.cloudyrock.mongock.ChangeSet; import com.github.cloudyrock.mongock.driver.mongodb.springdata.v4.decorator.impl.MongockTemplate; import com.github.f4b6a3.uuid.UuidCreator; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import com.mongodb.client.result.DeleteResult; import lombok.extern.slf4j.Slf4j; import org.bson.Document; import org.lowcoder.domain.application.model.Application; @@ -44,6 +47,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Arrays; import java.util.List; import java.util.Set; @@ -313,41 +317,86 @@ private int getMongoDBVersion(MongockTemplate mongoTemplate) { @ChangeSet(order = "026", id = "add-time-series-snapshot-history", author = "") public void addTimeSeriesSnapshotHistory(MongockTemplate mongoTemplate, CommonConfig commonConfig) { int mongoVersion = getMongoDBVersion(mongoTemplate); - if (mongoVersion < 5) { - log.warn("MongoDB version is below 5. Time-series collections are not supported. Upgrade the MongoDB version."); - } - // Create the time-series collection if it doesn't exist - if (!mongoTemplate.collectionExists(ApplicationHistorySnapshotTS.class)) { - if(mongoVersion < 5) { - mongoTemplate.createCollection(ApplicationHistorySnapshotTS.class); - } else { - mongoTemplate.createCollection(ApplicationHistorySnapshotTS.class, CollectionOptions.empty().timeSeries("createdAt")); + Instant thresholdDate = Instant.now().minus(commonConfig.getQuery().getAppSnapshotKeepDuration(), ChronoUnit.DAYS); + + if (mongoVersion >= 5) { + // MongoDB version >= 5: Use manual insert query + if (!mongoTemplate.collectionExists(ApplicationHistorySnapshotTS.class)) { + mongoTemplate.createCollection(ApplicationHistorySnapshotTS.class, + CollectionOptions.empty().timeSeries("createdAt")); } + + // Aggregation pipeline to fetch the records + List aggregationPipeline = Arrays.asList( + new Document("$match", new Document("createdAt", new Document("$lte", thresholdDate))), + new Document("$project", new Document() + .append("applicationId", 1) + .append("dsl", 1) + .append("context", 1) + .append("createdAt", 1) + .append("createdBy", 1) + .append("modifiedBy", 1) + .append("updatedAt", 1) + .append("id", "$_id")) // Map `_id` to `id` if needed + ); + + MongoCollection sourceCollection = mongoTemplate.getDb().getCollection("applicationHistorySnapshot"); + MongoCollection targetCollection = mongoTemplate.getDb().getCollection("applicationHistorySnapshotTS"); + + // Fetch results and insert them into the time-series collection + try (MongoCursor cursor = sourceCollection.aggregate(aggregationPipeline).iterator()) { + while (cursor.hasNext()) { + Document document = cursor.next(); + targetCollection.insertOne(document); // Insert into the time-series collection + } + } + + // Delete the migrated records + Query deleteQuery = new Query(Criteria.where("createdAt").gte(thresholdDate)); + DeleteResult deleteResult = mongoTemplate.remove(deleteQuery, ApplicationHistorySnapshot.class); + + log.info("Deleted {} records from the source collection.", deleteResult.getDeletedCount()); + } else { + // MongoDB version < 5: Use aggregation with $out + if (!mongoTemplate.collectionExists(ApplicationHistorySnapshotTS.class)) { + mongoTemplate.createCollection(ApplicationHistorySnapshotTS.class); // Create a regular collection + } + + // Aggregation pipeline with $out + List aggregationPipeline = Arrays.asList( + new Document("$match", new Document("createdAt", new Document("$lte", thresholdDate))), + new Document("$project", new Document() + .append("applicationId", 1) + .append("dsl", 1) + .append("context", 1) + .append("createdAt", 1) + .append("createdBy", 1) + .append("modifiedBy", 1) + .append("updatedAt", 1) + .append("id", "$_id")), // Map `_id` to `id` if needed + new Document("$out", "applicationHistorySnapshotTS") // Write directly to the target collection + ); + + mongoTemplate.getDb() + .getCollection("applicationHistorySnapshot") + .aggregate(aggregationPipeline) + .toCollection(); + + // Delete the migrated records + Query deleteQuery = new Query(Criteria.where("createdAt").gte(thresholdDate)); + DeleteResult deleteResult = mongoTemplate.remove(deleteQuery, ApplicationHistorySnapshot.class); + + log.info("Deleted {} records from the source collection.", deleteResult.getDeletedCount()); } - Instant thresholdDate = Instant.now().minus(commonConfig.getQuery().getAppSnapshotKeepDuration(), ChronoUnit.DAYS); - List snapshots = mongoTemplate.find(new Query().addCriteria(Criteria.where("createdAt").gte(thresholdDate)), ApplicationHistorySnapshot.class); - snapshots.forEach(snapshot -> { - ApplicationHistorySnapshotTS applicationHistorySnapshotTS = new ApplicationHistorySnapshotTS(); - applicationHistorySnapshotTS.setApplicationId(snapshot.getApplicationId()); - applicationHistorySnapshotTS.setDsl(snapshot.getDsl()); - applicationHistorySnapshotTS.setContext(snapshot.getContext()); - applicationHistorySnapshotTS.setCreatedAt(snapshot.getCreatedAt()); - applicationHistorySnapshotTS.setCreatedBy(snapshot.getCreatedBy()); - applicationHistorySnapshotTS.setModifiedBy(snapshot.getModifiedBy()); - applicationHistorySnapshotTS.setUpdatedAt(snapshot.getUpdatedAt()); - applicationHistorySnapshotTS.setId(snapshot.getId()); - mongoTemplate.insert(applicationHistorySnapshotTS); - mongoTemplate.remove(snapshot); - }); - // Ensure indexes if needed + // Ensure indexes on the new collection ensureIndexes(mongoTemplate, ApplicationHistorySnapshotTS.class, makeIndex("applicationId"), - makeIndex("createdAt") - ); + makeIndex("createdAt")); } + private void addGidField(MongockTemplate mongoTemplate, String collectionName) { // Create a query to match all documents Query query = new Query(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/MigrateAuthConfigJobImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/MigrateAuthConfigJobImpl.java index d866159595..a89eb4480a 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/MigrateAuthConfigJobImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/MigrateAuthConfigJobImpl.java @@ -8,6 +8,7 @@ import org.lowcoder.sdk.auth.AbstractAuthConfig; import org.lowcoder.sdk.config.AuthProperties; import org.lowcoder.sdk.config.CommonConfig; +import org.lowcoder.sdk.constants.AuthSourceConstants; import org.lowcoder.sdk.constants.WorkspaceMode; import org.lowcoder.sdk.util.IDUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -57,6 +58,6 @@ protected void setAuthConfigs2OrganizationDomain(Organization organization, List organization.setOrganizationDomain(domain); } authConfigs.forEach(abstractAuthConfig -> abstractAuthConfig.setId(IDUtils.generate())); - domain.setConfigs(authConfigs); + domain.setConfigs(authConfigs.stream().filter(authConfig -> !authConfig.getSource().equals(AuthSourceConstants.EMAIL)).toList()); } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/task/ArchiveSnapshotTask.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/task/ArchiveSnapshotTask.java index 2fa5163799..28108f51a0 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/task/ArchiveSnapshotTask.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/task/ArchiveSnapshotTask.java @@ -2,12 +2,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; import org.lowcoder.sdk.config.CommonConfig; import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -16,6 +12,11 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import com.mongodb.client.model.Filters; +import org.bson.Document; + @Slf4j @RequiredArgsConstructor @Component @@ -24,23 +25,122 @@ public class ArchiveSnapshotTask { private final CommonConfig commonConfig; private final MongoTemplate mongoTemplate; - @Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.DAYS) + @Scheduled(initialDelay = 0, fixedRate = 1, timeUnit = TimeUnit.DAYS) public void archive() { + int mongoVersion = getMongoDBVersion(); Instant thresholdDate = Instant.now().minus(commonConfig.getQuery().getAppSnapshotKeepDuration(), ChronoUnit.DAYS); - List snapshots = mongoTemplate.find(new Query().addCriteria(Criteria.where("createdAt").lte(thresholdDate)), ApplicationHistorySnapshotTS.class); - snapshots.forEach(snapshot -> { - ApplicationHistorySnapshot applicationHistorySnapshot = new ApplicationHistorySnapshot(); - applicationHistorySnapshot.setApplicationId(snapshot.getApplicationId()); - applicationHistorySnapshot.setDsl(snapshot.getDsl()); - applicationHistorySnapshot.setContext(snapshot.getContext()); - applicationHistorySnapshot.setCreatedAt(snapshot.getCreatedAt()); - applicationHistorySnapshot.setCreatedBy(snapshot.getCreatedBy()); - applicationHistorySnapshot.setModifiedBy(snapshot.getModifiedBy()); - applicationHistorySnapshot.setUpdatedAt(snapshot.getUpdatedAt()); - applicationHistorySnapshot.setId(snapshot.getId()); - mongoTemplate.insert(applicationHistorySnapshot); - mongoTemplate.remove(snapshot); - }); + + if (mongoVersion >= 5) { + archiveForVersion5AndAbove(thresholdDate); + } else { + archiveForVersionBelow5(thresholdDate); + } + } + + private int getMongoDBVersion() { + Document buildInfo = mongoTemplate.getDb().runCommand(new Document("buildInfo", 1)); + String version = buildInfo.getString("version"); + return Integer.parseInt(version.split("\\.")[0]); // Parse major version } + private void archiveForVersion5AndAbove(Instant thresholdDate) { + log.info("Running archival for MongoDB version >= 5"); + + MongoCollection sourceCollection = mongoTemplate.getDb().getCollection("applicationHistorySnapshot"); + MongoCollection targetCollection = mongoTemplate.getDb().getCollection("applicationHistorySnapshotTS"); + + long totalDocuments = sourceCollection.countDocuments(Filters.lte("createdAt", thresholdDate)); + log.info("Total documents to archive: {}", totalDocuments); + + long processedCount = 0; + + try (MongoCursor cursor = sourceCollection.find(Filters.lte("createdAt", thresholdDate)).iterator()) { + while (cursor.hasNext()) { + Document document = cursor.next(); + + // Transform the document for the target collection + document.put("id", document.getObjectId("_id")); // Map `_id` to `id` + document.remove("_id"); + + // Insert the document into the target collection + try { + targetCollection.insertOne(document); + } catch (Exception e) { + log.error("Failed to insert document with ID {}. Error: {}", document.getObjectId("id"), e.getMessage()); + continue; + } + + // Remove the document from the source collection + try { + sourceCollection.deleteOne(Filters.eq("_id", document.getObjectId("id"))); + } catch (Exception e) { + log.error("Failed to delete document with ID {}. Error: {}", document.getObjectId("id"), e.getMessage()); + continue; + } + + processedCount++; + log.info("Processed document {} / {}", processedCount, totalDocuments); + } + } catch (Exception e) { + log.error("Failed during archival process. Error: {}", e.getMessage()); + } + + log.info("Archival process completed. Total documents archived: {}", processedCount); + } + + private void archiveForVersionBelow5(Instant thresholdDate) { + log.info("Running archival for MongoDB version < 5"); + + MongoCollection sourceCollection = mongoTemplate.getDb().getCollection("applicationHistorySnapshot"); + + long totalDocuments = sourceCollection.countDocuments(Filters.lte("createdAt", thresholdDate)); + log.info("Total documents to archive: {}", totalDocuments); + + long processedCount = 0; + + try (MongoCursor cursor = sourceCollection.find(Filters.lte("createdAt", thresholdDate)).iterator()) { + while (cursor.hasNext()) { + Document document = cursor.next(); + + // Transform the document for the target collection + document.put("id", document.getObjectId("_id")); // Map `_id` to `id` + document.remove("_id"); + + // Use aggregation with $out for the single document + try { + sourceCollection.aggregate(List.of( + Filters.eq("_id", document.getObjectId("id")), + new Document("$project", new Document() + .append("applicationId", document.get("applicationId")) + .append("dsl", document.get("dsl")) + .append("context", document.get("context")) + .append("createdAt", document.get("createdAt")) + .append("createdBy", document.get("createdBy")) + .append("modifiedBy", document.get("modifiedBy")) + .append("updatedAt", document.get("updatedAt")) + .append("id", document.get("id"))), + new Document("$out", "applicationHistorySnapshotTS") + )).first(); + } catch (Exception e) { + log.error("Failed to aggregate and insert document with ID {}. Error: {}", document.getObjectId("id"), e.getMessage()); + continue; + } + + // Remove the document from the source collection + try { + sourceCollection.deleteOne(Filters.eq("_id", document.getObjectId("id"))); + } catch (Exception e) { + log.error("Failed to delete document with ID {}. Error: {}", document.getObjectId("id"), e.getMessage()); + continue; + } + + processedCount++; + log.info("Processed document {} / {}", processedCount, totalDocuments); + } + } catch (Exception e) { + log.error("Failed during archival process. Error: {}", e.getMessage()); + } + + log.info("Archival process completed. Total documents archived: {}", processedCount); + } } diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java index c470c11d05..09fa8a2b99 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java @@ -137,7 +137,7 @@ public void updateByGid() { public void move() { Mono> mono = folderApiService.move("app01", "folder02") - .then(folderApiService.getElements("folder02", null, null).collectList()); + .then(folderApiService.getElements("folder02", null, null, null).collectList()); StepVerifier.create(mono) .assertNext(list -> { diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java index fb7109134d..81c0cb56d1 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java @@ -4,7 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.junit.runner.RunWith; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; import org.lowcoder.domain.application.service.ApplicationHistorySnapshotService; import org.lowcoder.sdk.models.HasIdAndAuditing; import org.springframework.beans.factory.annotation.Autowired; @@ -47,8 +47,8 @@ public void testServiceMethods() { .assertNext(list -> { assertEquals(2, list.size()); - ApplicationHistorySnapshotTS first = list.get(0); - ApplicationHistorySnapshotTS second = list.get(1); + ApplicationHistorySnapshot first = list.get(0); + ApplicationHistorySnapshot second = list.get(1); assertTrue(first.getCreatedAt().isAfter(second.getCreatedAt())); assertNull(first.getDsl()); @@ -66,7 +66,7 @@ public void testServiceMethods() { StepVerifier.create(service.listAllHistorySnapshotBriefInfo(applicationId, null, null, null, null, PageRequest.of(1, 1))) .assertNext(list -> { assertEquals(1, list.size()); - ApplicationHistorySnapshotTS one = list.get(0); + ApplicationHistorySnapshot one = list.get(0); assertNull(one.getDsl()); assertEquals(ImmutableMap.of("context", "context1"), one.getContext()); assertEquals(applicationId, one.getApplicationId()); diff --git a/server/api-service/pom.xml b/server/api-service/pom.xml index 534f9e069b..ab8b7a98aa 100644 --- a/server/api-service/pom.xml +++ b/server/api-service/pom.xml @@ -12,7 +12,7 @@ - 2.5.0 + 2.5.1 17 ${java.version} ${java.version} diff --git a/server/node-service/package.json b/server/node-service/package.json index 4cd6aa0583..28cd1b0f13 100644 --- a/server/node-service/package.json +++ b/server/node-service/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-node-server", - "version": "2.5.0", + "version": "2.5.1", "private": true, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -61,8 +61,8 @@ "jsonpath": "^1.1.1", "lodash": "^4.17.21", "loglevel": "^1.8.1", - "lowcoder-core": "^0.0.8", - "lowcoder-sdk": "0.0.41", + "lowcoder-core": "^0.0.10", + "lowcoder-sdk": "2.4.17", "morgan": "^1.10.0", "node-fetch": "2", "node-firebird": "^1.1.9", diff --git a/server/node-service/src/common/util.ts b/server/node-service/src/common/util.ts index 821c076399..216b0e272b 100644 --- a/server/node-service/src/common/util.ts +++ b/server/node-service/src/common/util.ts @@ -128,6 +128,6 @@ export function dirToSpecList(specDir: string) { spec, }); }); - logger.info("spec list loaded %s, duration: %d ms",specDir, performance.now() - start); + // logger.info("spec list loaded %s, duration: %d ms",specDir, performance.now() - start); return specList; } \ No newline at end of file diff --git a/server/node-service/src/plugins/apiTemplate/apiTemplate.spec.json b/server/node-service/src/plugins/apiTemplate/apiTemplate.spec.json new file mode 100644 index 0000000000..ee8684866a --- /dev/null +++ b/server/node-service/src/plugins/apiTemplate/apiTemplate.spec.json @@ -0,0 +1,1799 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "# Introduction\n\n\nWelcome to the [APITemplate.io](https://apitemplate.io) API v2!\n\nAPITemplate.io provides PDF generation services including [Template-based PDF generation](https://apitemplate.io/pdf-generation-api/), [HTML to PDF](https://apitemplate.io/html-to-pdf-api/), and [URL to PDF conversions](https://apitemplate.io/create-pdf-from-url/), as well as an [image generation API](https://apitemplate.io/image-generation-api/).\n\nThis page contains the documentation on how to use APITemplate.io through API calls. With the APITemplate.io API, you can create PDF documents and images, as well as manage your templates.\n\nOur API is built on RESTful HTTP, so you can utilize any HTTP/REST library of your choice in your preferred programming language to interact with APITemplate.io's API.\n\n**Steps to produce PDFs/Images**\n1. Design your template(s) using our intuitive drag-and-drop template editor or the HTML editor and save it.\n2. Integrate your workflow, either with platforms like Zapier, Make.com/Integromat, Bubble.io, or any programming languages that support REST API, to send us the JSON data along with the template ID/URL/or HTML content.\n3. Our REST API will then return a download URL for the images (in PNG and JPEG formats) or PDFs.\n\n# Authentication\nUpon signing up for an account, an API key will be generated for you. If needed, you can reset this API key via the web console (under the \"API Integration\" section).\n\nTo integrate with our services, you need to authenticate with the APITemplate.io API. Provide your secret key in the request header using the X-API-KEY field.\n\n\n# Content Type and CORS\n\n**Request Content-Type**\nThe Content-Type for POST and GET requests is set to application/json.\n\n**Cross-Origin Resource Sharing**\nThis API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/).\nAnd that allows cross-domain communication from the browser.\nAll responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site.\n\n\n\n# Regional API endpoint(s)\nA regional API endpoint is intended for customers in the same region. The data for the requests and generated PDFs/images are processed and stored within the region.\n\nThe regions are:\n\n| Region | Endpoint | Max Timeout (Seconds) | Max Payload Size(MB)** |\n|----------------------|-------------------------------------|-----------------------|-------------------------|\n| Default (Singapore) | https://rest.apitemplate.io | 100 | 1 |\n| Europe (Frankfurt) | https://rest-de.apitemplate.io | 100 | 1 |\n| US East (N. Virginia)| https://rest-us.apitemplate.io | 100 | 1 |\n| Australia (Sydney) | https://rest-au.apitemplate.io | 30 | 6 |\n\n\nAlternative Regions:\n| Region | Endpoint | Max Timeout (Seconds) | Max Payload Size(MB)** |\n|----------------------|-------------------------------------|-----------------------|-------------------------|\n| Default (Singapore) | https://rest-alt.apitemplate.io | 30 | 6 |\n| Europe (Frankfurt) | https://rest-alt-de.apitemplate.io | 30 | 6 |\n| US East (N. Virginia)| https://rest-alt-us.apitemplate.io | 30 | 6 |\n\n** Note:\n- Payload size applies to request and response\n- If \"export_type\" is set to `json` which output file that on AWS S3 doesn't have the limitation\n- If the \"export_type\" is set to `file` which returns binary data of the generated PDF, the file size of the generated PDF is limited to either 6MB or 1MB based on the region\n\n\n\nOther regions are available on request, contact us at hello@apitemplate.io for more information\n\n# Rate limiting\nOur API endpoints use IP-based rate limiting to ensure fair usage and prevent abuse. Users are allowed to make up to **100 requests per 10 seconds**. This rate limit is designed to accommodate a reasonable volume of requests while maintaining optimal performance for all users.\n\nHowever, if you exceed this limit and make additional requests, you will receive a response with HTTP code 429. This status code indicates that you have reached the rate limit and need to wait before making further requests.\n", + "version": "Version 2.0", + "title": "APITemplate.io API Reference", + "termsOfService": "https://apitemplate.io/privacy-policy/", + "contact": { + "email": "hello@apitemplate.io", + "url": "https://apitemplate.io" + }, + "x-logo": { + "url": "images/logo_new2_with_text2.png", + "altText": "APITemplate.io logo" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "servers": [ + { + "url": "https://rest.apitemplate.io" + }, + { + "url": "https://rest-au.apitemplate.io" + }, + { + "url": "https://rest-de.apitemplate.io" + }, + { + "url": "https://rest-us.apitemplate.io" + } + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "paths": { + "/v2/create-pdf": { + "post": { + "summary": "Create a PDF", + "operationId": "create-pdf", + "description": "This endpoint creates a PDF file with JSON data and your template. We support synchoronus and asynchronous PDF generation.", + "tags": [ + "API Integration" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/paramTemplateID" + }, + { + "$ref": "#/components/parameters/paramExportType" + }, + { + "$ref": "#/components/parameters/paramExportInBase64" + }, + { + "$ref": "#/components/parameters/paramExpiration" + }, + { + "$ref": "#/components/parameters/paramOutputHTML" + }, + { + "$ref": "#/components/parameters/paramOutputFormat" + }, + { + "$ref": "#/components/parameters/paramFileName" + }, + { + "$ref": "#/components/parameters/paramDirectDownload" + }, + { + "$ref": "#/components/parameters/paramCloudStorage" + }, + { + "$ref": "#/components/parameters/paramLoadDataFrom" + }, + { + "$ref": "#/components/parameters/paramGenerationDelay" + }, + { + "$ref": "#/components/parameters/paramImageResampleRes" + }, + { + "$ref": "#/components/parameters/paramResizeImages" + }, + { + "$ref": "#/components/parameters/paramResizeMaxWidth" + }, + { + "$ref": "#/components/parameters/paramResizeMaxHeight" + }, + { + "$ref": "#/components/parameters/paramResizeFormat" + }, + { + "$ref": "#/components/parameters/paramPOSTACTIONS3FILEKEY" + }, + { + "$ref": "#/components/parameters/paramPOSTACTIONS3BUCKET" + }, + { + "$ref": "#/components/parameters/paramMeta" + }, + { + "$ref": "#/components/parameters/paramAsync" + }, + { + "$ref": "#/components/parameters/paramWebhook" + }, + { + "$ref": "#/components/parameters/paramWebhookMethod" + }, + { + "$ref": "#/components/parameters/paramWebhookHeaders" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "JSON data", + "example": { + "invoice_number": "INV38379", + "date": "2021-09-30", + "currency": "USD", + "total_amount": 82542.56 + } + } + } + } + }, + "responses": { + "200": { + "description": "Returns status and output file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseSuccessPDFFile" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-code-samples": [ + { + "lang": "cURL", + "source": "curl --header \"Content-Type: application/json\" \\\n-H 'X-API-KEY: 6fa6g2pdXGIyHRhVlGh7U56Ada1eF' \\\n--data '{ \"invoice_number\": \"INV38379\", \"date\": \"2021-09-30\", \"currency\": \"USD\", \"total_amount\": 82542.56 }' \\\n\"https://rest.apitemplate.io/v2/create-pdf?template_id=79667b2b1876e347\"\n" + }, + { + "lang": "Python", + "source": "import requests, json\n\ndef main():\n api_key = \"6fa6g2pdXGIyHRhVlGh7U56Ada1eF\"\n template_id = \"79667b2b1876e347\"\n\n data = {\n \"invoice_number\": \"INV38379\",\n \"date\": \"2021-09-30\",\n \"currency\": \"USD\",\n \"total_amount\": 82542.56\n }\n\n response = requests.post(\n F\"https://rest.apitemplate.io/v2/create-pdf?template_id={template_id}\",\n headers = {\"X-API-KEY\": F\"{api_key}\"},\n json= data\n )\n\nif __name__ == \"__main__\":\n main()\n" + }, + { + "lang": "PHP", + "source": "\n" + }, + { + "lang": "Node.js", + "source": "const https = require('https');\nconst http = require('http');\nconst { URL } = require('url');\n\n(async () => {\n let resp = await httpPost(\n 'https://rest.apitemplate.io/v2/create-pdf?template_id=79667b2b1876e347',\n '{ \"invoice_number\": \"INV38379\", \"date\": \"2021-09-30\", \"currency\": \"USD\", \"total_amount\": 82542.56 }',\n '6fa6g2pdXGIyHRhVlGh7U56Ada1eF'\n );\n console.log(resp);\n})();\n\n\nasync function httpPost(url_api, data, apiKey){\n const uri = new URL(url_api);\n const fx = uri.protocol === 'https:' ? https : http;\n const opts = {\n method: 'POST',\n hostname: uri.hostname,\n port: uri.port,\n path: `${uri.pathname}${uri.search==null?\"\":uri.search}`,\n protocol: uri.protocol,\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': data.length,\n \"X-API-KEY\": apiKey\n }\n };\n\n return new Promise((resolve, reject) => {\n const req = fx.request(opts, (res) => {\n res.setEncoding('utf8');\n let responseBody = '';\n res.on('data', (chunk) => responseBody += chunk);\n res.on('end', () => resolve(responseBody));\n });\n\n req.on('error', (err) => reject(err));\n req.write(data)\n req.end();\n });\n}\n" + }, + { + "lang": "CSharp", + "source": "using System;\nusing System.IO;\nusing System.Net.Http;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nnamespace csharp\n{\n class ReturnContent{\n public string download_url{get;set;}\n public string status{get;set;}\n }\n\n class Program\n {\n static async Task Main(string[] args)\n {\n var api_key = \"6fa6g2pdXGIyHRhVlGh7U56Ada1eF\";\n var template_id = \"79667b2b1876e347\";\n var url = $\"https://rest.apitemplate.io/v2/create-pdf?template_id={template_id}\";\n\n var data = new {\n invoice_number = \"INV38379\",\n date = \"2021-09-30\",\n currency = \"USD\",\n total_amount = 82542.56\n };\n\n\n var json_content = JsonSerializer.Serialize(data);\n var buffer = System.Text.Encoding.UTF8.GetBytes(json_content);\n var byteContent = new ByteArrayContent(buffer);\n\n Console.WriteLine(json_content);\n\n var client = new HttpClient();\n client.DefaultRequestHeaders.Add(\"X-API-KEY\",api_key);\n var response = await client.PostAsync(url,byteContent);\n var ret = await response.Content.ReadAsStringAsync();\n\n var returnContent = JsonSerializer.Deserialize(ret);\n\n if(returnContent.status==\"success\"){\n Console.WriteLine($\"Downloading {returnContent.download_url}...\");\n var download_response = await client.GetAsync(returnContent.download_url);\n using (var stream = await download_response.Content.ReadAsStreamAsync())\n {\n var fileInfo = new FileInfo(\"image.jpeg\");\n using (var fileStream = fileInfo.OpenWrite())\n {\n await stream.CopyToAsync(fileStream);\n }\n }\n }\n }\n }\n}\n" + } + ] + } + }, + "/v2/create-image": { + "post": { + "summary": "Create an Image", + "operationId": "create-image", + "description": "This endpoint creates a JPEG file(along with PNG) with JSON data and your template\n", + "tags": [ + "API Integration" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/paramTemplateID" + }, + { + "in": "query", + "name": "output_image_type", + "schema": { + "type": "string" + }, + "required": false, + "description": "- Output image type(JPEG or PNG format), default to `all`. Options are `all`, `jpegOnly`,`pngOnly`.\n", + "example": "1" + }, + { + "$ref": "#/components/parameters/paramExpiration" + }, + { + "$ref": "#/components/parameters/paramCloudStorage" + }, + { + "$ref": "#/components/parameters/paramGenerationDelay" + }, + { + "$ref": "#/components/parameters/paramPOSTACTIONS3FILEKEY" + }, + { + "$ref": "#/components/parameters/paramPOSTACTIONS3BUCKET" + }, + { + "$ref": "#/components/parameters/paramMeta" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "JSON data\n- The following is the json format in the post body to generate an image\n```\n{\n \"overrides\": [\n {\n \"name\": \"\",\n \"property_1\": \"\",\n \"property_2\": \"\",\n \"property_3\": \"\",\n ...\n },\n {\n \"name\": \"\",\n \"property_2\": \"\",\n ...\n }\n ]\n}\n```\n", + "example": { + "overrides": [ + { + "name": "text_1", + "text": "hello world", + "textBackgroundColor": "rgba(246, 243, 243, 0)" + }, + { + "name": "image_1", + "src": "https://via.placeholder.com/150" + } + ] + } + } + } + } + }, + "responses": { + "200": { + "description": "Returns status and output file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseSuccessImageFile" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-code-samples": [ + { + "lang": "cURL", + "source": "curl --header \"Content-Type: application/json\" \\\n-H 'X-API-KEY: 6fa6g2pdXGIyHRhVlGh7U56Ada1eF' \\\n--data '{ \"overrides\":[ { \"name\":\"text_1\", \"text\":\"hello world\", \"textBackgroundColor\":\"rgba(246, 243, 243, 0)\" }, { \"name\":\"image_1\", \"src\":\"https://via.placeholder.com/150\" } ] }' \\\n\"https://rest.apitemplate.io/v2/create-image?template_id=79667b2b1876e347\"\n" + }, + { + "lang": "Python", + "source": "import requests, json\n\ndef main():\n api_key = \"6fa6g2pdXGIyHRhVlGh7U56Ada1eF\"\n template_id = \"79667b2b1876e347\"\n\n data = {\n \"overrides\":[\n {\n \"name\":\"text_1\",\n \"text\":\"hello world\",\n \"textBackgroundColor\":\"rgba(246, 243, 243, 0)\"\n },\n {\n \"name\":\"image_1\",\n \"src\":\"https://via.placeholder.com/150\"\n }\n ]\n }\n\n response = requests.post(\n F\"https://rest.apitemplate.io/v2/create-image?template_id={template_id}\",\n headers = {\"X-API-KEY\": F\"{api_key}\"},\n json= data\n )\n\nif __name__ == \"__main__\":\n main()\n" + }, + { + "lang": "PHP", + "source": "\n" + }, + { + "lang": "Node.js", + "source": "const https = require('https');\nconst http = require('http');\nconst { URL } = require('url');\n\n(async () => {\n let resp = await httpPost(\n 'https://rest.apitemplate.io/v2/create-pdf?template_id=79667b2b1876e347',\n '{ \"overrides\":[ { \"name\":\"text_1\", \"text\":\"hello world\", \"textBackgroundColor\":\"rgba(246, 243, 243, 0)\" }, { \"name\":\"image_1\", \"src\":\"https://via.placeholder.com/150\" } ] }',\n '6fa6g2pdXGIyHRhVlGh7U56Ada1eF'\n );\n console.log(resp);\n})();\n\n\nasync function httpPost(url_api, data, apiKey){\n const uri = new URL(url_api);\n const fx = uri.protocol === 'https:' ? https : http;\n const opts = {\n method: 'POST',\n hostname: uri.hostname,\n port: uri.port,\n path: `${uri.pathname}${uri.search==null?\"\":uri.search}`,\n protocol: uri.protocol,\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': data.length,\n \"X-API-KEY\": apiKey\n }\n };\n\n return new Promise((resolve, reject) => {\n const req = fx.request(opts, (res) => {\n res.setEncoding('utf8');\n let responseBody = '';\n res.on('data', (chunk) => responseBody += chunk);\n res.on('end', () => resolve(responseBody));\n });\n\n req.on('error', (err) => reject(err));\n req.write(data)\n req.end();\n });\n}\n" + }, + { + "lang": "CSharp", + "source": "using System;\nusing System.IO;\nusing System.Net.Http;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nnamespace csharp\n{\n class ReturnContent{\n public string download_url{get;set;}\n public string status{get;set;}\n }\n\n class Program\n {\n static async Task Main(string[] args)\n {\n var api_key = \"6fa6g2pdXGIyHRhVlGh7U56Ada1eF\";\n var template_id = \"79667b2b1876e347\";\n var url = $\"https://rest.apitemplate.io/v2/create-image?template_id={template_id}\";\n\n var json_content = '{ \"overrides\":[ { \"name\":\"text_1\", \"text\":\"hello world\", \"textBackgroundColor\":\"rgba(246, 243, 243, 0)\" }, { \"name\":\"text_2\", \"text\":\"Hi there\" } ] }';\n\n var buffer = System.Text.Encoding.UTF8.GetBytes(json_content);\n var byteContent = new ByteArrayContent(buffer);\n\n Console.WriteLine(json_content);\n\n var client = new HttpClient();\n client.DefaultRequestHeaders.Add(\"X-API-KEY\",api_key);\n var response = await client.PostAsync(url,byteContent);\n var ret = await response.Content.ReadAsStringAsync();\n\n var returnContent = JsonSerializer.Deserialize(ret);\n\n if(returnContent.status==\"success\"){\n Console.WriteLine($\"Downloading {returnContent.download_url}...\");\n var download_response = await client.GetAsync(returnContent.download_url);\n using (var stream = await download_response.Content.ReadAsStreamAsync())\n {\n var fileInfo = new FileInfo(\"image.jpeg\");\n using (var fileStream = fileInfo.OpenWrite())\n {\n await stream.CopyToAsync(fileStream);\n }\n }\n }\n }\n }\n }\n" + } + ] + } + }, + "/v2/create-pdf-from-html": { + "post": { + "summary": "Create a PDF from HTML", + "operationId": "create-pdf-from-html", + "description": "- This endpoint creates a PDF file from HTML with JSON data\n", + "tags": [ + "API Integration" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/paramExportType" + }, + { + "$ref": "#/components/parameters/paramExpiration" + }, + { + "$ref": "#/components/parameters/paramOutputFormat" + }, + { + "$ref": "#/components/parameters/paramFileName" + }, + { + "$ref": "#/components/parameters/paramDirectDownload" + }, + { + "$ref": "#/components/parameters/paramCloudStorage" + }, + { + "$ref": "#/components/parameters/paramGenerationDelay" + }, + { + "$ref": "#/components/parameters/paramImageResampleRes" + }, + { + "$ref": "#/components/parameters/paramResizeImages" + }, + { + "$ref": "#/components/parameters/paramResizeMaxWidth" + }, + { + "$ref": "#/components/parameters/paramResizeMaxHeight" + }, + { + "$ref": "#/components/parameters/paramResizeFormat" + }, + { + "$ref": "#/components/parameters/paramPOSTACTIONS3FILEKEY" + }, + { + "$ref": "#/components/parameters/paramPOSTACTIONS3BUCKET" + }, + { + "$ref": "#/components/parameters/paramMeta" + }, + { + "$ref": "#/components/parameters/paramAsync" + }, + { + "$ref": "#/components/parameters/paramWebhook" + }, + { + "$ref": "#/components/parameters/paramWebhookMethod" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string", + "description": "The HTML body content for the PDF. This property supports HTML markup and can include Jinja2 syntax (e.g {{name}}). The value of {{name}} will be replaced with the actual value provided in the data object.\n", + "example": "

hello world {{name}}

" + }, + "css": { + "type": "string", + "description": "The CSS styles to be applied to the PDF. This property should contain valid CSS markup and should also include the style tag.\n", + "example": "" + }, + "data": { + "type": "object", + "description": "The data object containing values for dynamic content in the HTML body. This object should include properties with corresponding values.\n", + "example": { + "name": "This is a title" + } + }, + "settings": { + "$ref": "#/components/schemas/PDFGenerationSettingsObject" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Returns status and output file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseSuccessPDFFile" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-code-samples": [ + { + "lang": "Python", + "source": "import requests, json\n\ndef main():\n api_key = \"6fa6g2pdXGIyHRhVlGh7U56Ada1eF\"\n template_id = \"79667b2b1876e347\"\n\n data = {\n \"body\": \"

hello world {{name}}

\",\n \"css\": \"\",\n \"data\": {\n \"name\": \"This is a title\"\n },\n \"settings\": {\n \"paper_size\": \"A4\",\n \"orientation\": \"1\",\n \"header_font_size\": \"9px\",\n \"margin_top\": \"40\",\n \"margin_right\": \"10\",\n \"margin_bottom\": \"40\",\n \"margin_left\": \"10\",\n \"print_background\": \"1\",\n \"displayHeaderFooter\": true,\n \"custom_header\": \"\\n
\\n \\n \\n \\n \\n \\n
\",\n \"custom_footer\": \"\\n\\n \\n \\n \\n \\n \\n
\"\n }\n }\n\n response = requests.post(\n F\"https://rest.apitemplate.io/v2/create-pdf-from-html\",\n headers = {\"X-API-KEY\": F\"{api_key}\"},\n json= data\n )\n\nif __name__ == \"__main__\":\n main()\n" + } + ] + } + }, + "/v2/create-pdf-from-url": { + "post": { + "summary": "Create a PDF from URL", + "operationId": "create-pdf-from-url", + "description": "- This endpoint creates a PDF file from a URL\n", + "tags": [ + "API Integration" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/paramExportType" + }, + { + "$ref": "#/components/parameters/paramExpiration" + }, + { + "$ref": "#/components/parameters/paramOutputFormat" + }, + { + "$ref": "#/components/parameters/paramFileName" + }, + { + "$ref": "#/components/parameters/paramDirectDownload" + }, + { + "$ref": "#/components/parameters/paramCloudStorage" + }, + { + "$ref": "#/components/parameters/paramGenerationDelay" + }, + { + "$ref": "#/components/parameters/paramImageResampleRes" + }, + { + "$ref": "#/components/parameters/paramResizeImages" + }, + { + "$ref": "#/components/parameters/paramResizeMaxWidth" + }, + { + "$ref": "#/components/parameters/paramResizeMaxHeight" + }, + { + "$ref": "#/components/parameters/paramResizeFormat" + }, + { + "$ref": "#/components/parameters/paramPOSTACTIONS3FILEKEY" + }, + { + "$ref": "#/components/parameters/paramPOSTACTIONS3BUCKET" + }, + { + "$ref": "#/components/parameters/paramMeta" + }, + { + "$ref": "#/components/parameters/paramAsync" + }, + { + "$ref": "#/components/parameters/paramWebhook" + }, + { + "$ref": "#/components/parameters/paramWebhookMethod" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The URL\n", + "example": "https://en.wikipedia.org/wiki/Sceloporus_malachiticus" + }, + "settings": { + "$ref": "#/components/schemas/PDFGenerationSettingsObject" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Returns status and output file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseSuccessPDFFile" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-code-samples": [ + { + "lang": "Python", + "source": "import requests, json\n\ndef main():\n api_key = \"6fa6g2pdXGIyHRhVlGh7U56Ada1eF\"\n template_id = \"79667b2b1876e347\"\n\n data = {\n \"url\": \"https://en.wikipedia.org/wiki/Sceloporus_malachiticus\",\n \"settings\": {\n \"paper_size\": \"A4\",\n \"orientation\": \"1\",\n \"header_font_size\": \"9px\",\n \"margin_top\": \"40\",\n \"margin_right\": \"10\",\n \"margin_bottom\": \"40\",\n \"margin_left\": \"10\",\n \"print_background\": \"1\",\n \"displayHeaderFooter\": true,\n \"custom_header\": \"\\n\\n \\n \\n \\n \\n \\n
\",\n \"custom_footer\": \"\\n\\n \\n \\n \\n \\n \\n
\"\n }\n }\n\n response = requests.post(\n F\"https://rest.apitemplate.io/v2/create-pdf-from-url\",\n headers = {\"X-API-KEY\": F\"{api_key}\"},\n json= data\n )\n\nif __name__ == \"__main__\":\n main()\n" + } + ] + } + }, + "/v2/list-objects": { + "get": { + "summary": "List Generated Objects", + "operationId": "list-objects", + "description": "Retrieves all the generated PDFs and images\n", + "tags": [ + "API Integration" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "limit", + "schema": { + "type": "string" + }, + "required": false, + "description": "Retrieve only the number of records specified. Default to 300", + "example": 300 + }, + { + "in": "query", + "name": "offset", + "schema": { + "type": "string" + }, + "required": false, + "description": "Offset is used to skip the number of records from the results. Default to 0", + "example": 0 + }, + { + "in": "query", + "name": "template_id", + "schema": { + "type": "string" + }, + "required": false, + "description": "Filtered by template id", + "example": "00377b2b1e0ee394" + }, + { + "in": "query", + "name": "transaction_type", + "schema": { + "type": "string" + }, + "required": false, + "description": "Filtered by transaction type, options are `PDF`, `JPEG` or `MERGE`", + "example": "MERGE" + }, + { + "in": "query", + "name": "transaction_ref", + "schema": { + "type": "string" + }, + "required": false, + "description": "Transaction reference", + "example": "4adfhg-d0e8-7399-9335-717a881dd91" + } + ], + "responses": { + "200": { + "description": "Returns status and output file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseSuccessListObjects" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-code-samples": [ + { + "lang": "cURL", + "source": "curl --header \"Content-Type: application/json\" \\\n-H 'X-API-KEY: 6fa6g2pdXGIyHRhVlGh7U56Ada1eF' \\\n\"https://rest.apitemplate.io/v2/list-objects\"\n" + }, + { + "lang": "Python", + "source": "import requests, json\n\ndef main():\n api_key = \"6fa6g2pdXGIyHRhVlGh7U56Ada1eF\"\n\n response = requests.get(\n F\"https://rest.apitemplate.io/v2/list-objects\",\n headers = {\"X-API-KEY\": F\"{api_key}\"},\n )\n\nif __name__ == \"__main__\":\n main()\n" + }, + { + "lang": "PHP", + "source": "\n" + }, + { + "lang": "Node.js", + "source": "const https = require('https');\nconst http = require('http');\nconst { URL } = require('url');\n\n(async () => {\n let resp = await httpGet(\n 'https://rest.apitemplate.io/v2/list-objects',\n 'f6caMToxOjRySHV6dTRldU9JTVNobDg'\n );\n console.log(resp);\n})();\n\nasync function httpGet(url_api, apiKey){\n const uri = new URL(url_api);\n const fx = uri.protocol === 'https:' ? https : http;\n const opts = {\n method: 'GET',\n hostname: uri.hostname,\n port: uri.port,\n path: `${uri.pathname}${uri.search==null?\"\":uri.search}`,\n protocol: uri.protocol,\n headers: {\n \"X-API-KEY\": apiKey\n }\n };\n\n return new Promise((resolve, reject) => {\n const req = fx.get(opts, (res) => {\n res.setEncoding('utf8');\n let responseBody = '';\n res.on('data', (chunk) => responseBody += chunk);\n res.on('end', () =>resolve(responseBody));\n });\n req.on('error', (err) => reject(err));\n });\n}\n" + } + ] + } + }, + "/v2/delete-object": { + "get": { + "summary": "Delete an Object", + "operationId": "delete-object", + "description": "Delete a PDF or an image from CDN and mark the transaction as deleted\n", + "tags": [ + "API Integration" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "transaction_ref", + "schema": { + "type": "string" + }, + "required": true, + "description": "Object transaction reference", + "example": "1618d386-2343-3d234-b9c7-99c82bb9f104" + } + ], + "responses": { + "200": { + "description": "Returns status and output file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseSuccessDeleteObject" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-code-samples": [ + { + "lang": "cURL", + "source": "curl --header \"Content-Type: application/json\" \\\n-H 'X-API-KEY: 6fa6g2pdXGIyHRhVlGh7U56Ada1eF' \\\n\"https://rest.apitemplate.io/v2/delete-object?transaction_ref=1618d386-2343-3d234-b9c7-99c82bb9f104\"\n" + }, + { + "lang": "Python", + "source": "import requests, json\n\ndef main():\n api_key = \"6fa6g2pdXGIyHRhVlGh7U56Ada1eF\"\n\n response = requests.get(\n F\"https://rest.apitemplate.io/v2/delete-object?transaction_ref=1618d386-2343-3d234-b9c7-99c82bb9f104\",\n headers = {\"X-API-KEY\": F\"{api_key}\"},\n )\n\nif __name__ == \"__main__\":\n main()\n" + }, + { + "lang": "PHP", + "source": "\n" + }, + { + "lang": "Node.js", + "source": "const https = require('https');\nconst http = require('http');\nconst { URL } = require('url');\n\n(async () => {\n let resp = await httpGet(\n 'https://rest.apitemplate.io/v2/delete-object?transaction_ref=1618d386-2343-3d234-b9c7-99c82bb9f104',\n 'f6caMToxOjRySHV6dTRldU9JTVNobDg'\n );\n console.log(resp);\n})();\n\nasync function httpGet(url_api, apiKey){\n const uri = new URL(url_api);\n const fx = uri.protocol === 'https:' ? https : http;\n const opts = {\n method: 'GET',\n hostname: uri.hostname,\n port: uri.port,\n path: `${uri.pathname}${uri.search==null?\"\":uri.search}`,\n protocol: uri.protocol,\n headers: {\n \"X-API-KEY\": apiKey\n }\n };\n\n return new Promise((resolve, reject) => {\n const req = fx.get(opts, (res) => {\n res.setEncoding('utf8');\n let responseBody = '';\n res.on('data', (chunk) => responseBody += chunk);\n res.on('end', () =>resolve(responseBody));\n });\n req.on('error', (err) => reject(err));\n });\n}\n" + } + ] + } + }, + "/v2/list-templates": { + "get": { + "summary": "List Templates", + "operationId": "list-templates", + "description": "Retrieves the information of templates\n", + "tags": [ + "Template Management" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "limit", + "schema": { + "type": "string" + }, + "required": false, + "description": "Retrieve only the number of records specified. Default to 300", + "example": "300" + }, + { + "in": "query", + "name": "offset", + "schema": { + "type": "string" + }, + "required": false, + "description": "Offset is used to skip the number of records from the results. Default to 0", + "example": "0" + }, + { + "in": "query", + "name": "format", + "schema": { + "type": "string" + }, + "required": false, + "description": "To filter the templates by either 'PDF' or 'JPEG'", + "example": "JPEG" + }, + { + "in": "query", + "name": "template_id", + "schema": { + "type": "string" + }, + "required": false, + "description": "To filter the templates by template id", + "example": "00377b2b1e0ee394" + }, + { + "in": "query", + "name": "group_name", + "schema": { + "type": "string" + }, + "required": false, + "description": "To filter the templates by the group name", + "example": "custom" + }, + { + "in": "query", + "name": "with_layer_info", + "schema": { + "type": "string" + }, + "required": false, + "description": "Return along with layer information for image templates, 0=false , 1=true. Default to '0'", + "example": 0 + } + ], + "responses": { + "200": { + "description": "Returns status and output file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseSuccessListTemplates" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-code-samples": [ + { + "lang": "cURL", + "source": "curl --header \"Content-Type: application/json\" \\\n-H 'X-API-KEY: 6fa6g2pdXGIyHRhVlGh7U56Ada1eF' \\\n\"https://rest.apitemplate.io/v2/list-templates\"\n" + }, + { + "lang": "Python", + "source": "import requests, json\n\ndef main():\n api_key = \"6fa6g2pdXGIyHRhVlGh7U56Ada1eF\"\n\n response = requests.get(\n F\"https://rest.apitemplate.io/v2/list-templates\",\n headers = {\"X-API-KEY\": F\"{api_key}\"},\n )\n\nif __name__ == \"__main__\":\n main()\n" + }, + { + "lang": "PHP", + "source": "\n" + }, + { + "lang": "Node.js", + "source": "const https = require('https');\nconst http = require('http');\nconst { URL } = require('url');\n\n(async () => {\n let resp = await httpGet(\n 'https://rest.apitemplate.io/v2/list-templates',\n 'f6caMToxOjRySHV6dTRldU9JTVNobDg'\n );\n console.log(resp);\n})();\n\nasync function httpGet(url_api, apiKey){\n const uri = new URL(url_api);\n const fx = uri.protocol === 'https:' ? https : http;\n const opts = {\n method: 'GET',\n hostname: uri.hostname,\n port: uri.port,\n path: `${uri.pathname}${uri.search==null?\"\":uri.search}`,\n protocol: uri.protocol,\n headers: {\n \"X-API-KEY\": apiKey\n }\n };\n\n return new Promise((resolve, reject) => {\n const req = fx.get(opts, (res) => {\n res.setEncoding('utf8');\n let responseBody = '';\n res.on('data', (chunk) => responseBody += chunk);\n res.on('end', () =>resolve(responseBody));\n });\n req.on('error', (err) => reject(err));\n });\n}\n" + } + ] + } + }, + "/v2/get-template": { + "get": { + "summary": "Get PDF template", + "operationId": "get-template", + "description": "Retrieves information of the PDF template (**This is an experimental API, contact support to learn more**)\n", + "tags": [ + "Template Management" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "template_id", + "schema": { + "type": "string" + }, + "required": false, + "description": "Your template id, it can be obtained in the web console(Manage Templates)", + "example": "00377b2b1e0ee394" + } + ], + "responses": { + "200": { + "description": "Returns status and template information", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseSuccessTemplate" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-code-samples": [ + { + "lang": "cURL", + "source": "curl --header \"Content-Type: application/json\" \\\n-H 'X-API-KEY: 6fa6g2pdXGIyHRhVlGh7U56Ada1eF' \\\n\"https://rest.apitemplate.io/v2/get-template?template_id=cd890b2b199c5c42\"\n" + } + ] + } + }, + "/v2/update-template": { + "post": { + "summary": "Update PDF Template", + "operationId": "update-template", + "description": "This endpoint updates PDF template (**This is an experimental API, contact support to learn more**)", + "tags": [ + "Template Management" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "template_id": { + "type": "string", + "description": "Your template id, it can be obtained in the web console(Manage Templates)\n", + "example": "00377b2b1e0ee394" + }, + "body": { + "type": "string", + "description": "The HTML body\n", + "example": "

Title

\n" + }, + "css": { + "type": "string", + "description": "The css\n", + "example": "{body{ background: white;}\n" + } + }, + "required": [ + "template_id" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Returns status and output file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseSuccess" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-code-samples": [ + { + "lang": "cURL", + "source": "curl -X POST \\\n --header \"Content-Type: application/json\" \\\n -H 'X-API-KEY: 6fa6g2pdXGIyHRhVlGh7U56Ada1eF' \\\n --data '{\"template_id\": \"d4477b2b2348d03a\",\"body\":\"

this is a title

\"}' \\\n \"https://rest.apitemplate.io/v2/update-template\"\n" + } + ] + } + }, + "/v2/merge-pdfs": { + "post": { + "summary": "Join/Merge multiple PDFs", + "operationId": "merge-pdfs", + "description": "This endpoint merges/joins multiple PDF URLs into a single PDF file", + "tags": [ + "PDF Manipulation API" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/paramPOSTACTIONS3FILEKEY" + }, + { + "$ref": "#/components/parameters/paramPOSTACTIONS3BUCKET" + }, + { + "$ref": "#/components/parameters/paramMeta" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "urls": { + "type": "array", + "items": { + "type": "object" + }, + "description": "URL array. We support normal http/https URLs and data URLs\n- Normal URLs: URLs start with http/https, e.g: \"https://fileserver.com/a1.pdf\")\n- Data URLs: URLs prefixed with the \"data:\" scheme, e.g \"data:application/pdf;base64,JVBERi0xLjIg...[truncated]\"\n", + "example": [ + "https://fileserver.com/a1.pdf", + "https://fileserver.com/b2.pdf", + "data:application/pdf;base64,JVBERi0xLjIg...[truncated]" + ] + }, + "export_type": { + "type": "string", + "description": "- Either `file` or `json`(Default).\n - The option `json` returns a JSON object, and the output PDF is stored on a CDN.\n - The option `file` returns binary data of the generated PDF(Secure and completely private) and the response HTTP header Content-Disposition is set to attachment. It has a file size limit of 6MB.\n", + "example": "json" + }, + "expiration": { + "type": "integer", + "description": "- Expiration of the generated PDF in minutes(default to `0`, store permanently)\n - Use `0` to store on cdn permanently\n - Or use the range between `1` minute and `43200` minutes(30 days) to specify the expiration of the generated PDF\n", + "example": 5 + }, + "cloud_storage": { + "type": "integer", + "description": "- Upload the generated PDFs/images to our storage CDN, default to `1`. If you have configured `Post Action` to upload the PDFs/Images to your own S3, please set it to `0`.\n", + "example": 1 + } + }, + "required": [ + "urls" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Returns status and output file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseSuccessSingleFile" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "x-code-samples": [ + { + "lang": "cURL", + "source": "curl -X POST \\\n --header \"Content-Type: application/json\" \\\n -H 'X-API-KEY: 6fa6g2pdXGIyHRhVlGh7U56Ada1eF' \\\n --data '{ \"urls\": [\"https://fileserver.com/a1.pdf\",\"https://fileserver.com/b2.pdf\"] }' \\\n \"https://rest.apitemplate.io/v2/merge-pdfs\"\n" + }, + { + "lang": "Python", + "source": "import requests, json\n\ndef main():\n api_key = \"6fa6g2pdXGIyHRhVlGh7U56Ada1eF\"\n\n json_payload = {\n \"urls\": [\"https://fileserver.com/a1.pdf\",\"https://fileserver.com/b2.pdf\"] ,\n \"output_file\": \"output.pdf\",\n }\n\n response = requests.post(\n F\"https://rest.apitemplate.io/v2/merge-pdfs\",\n headers = {\"X-API-KEY\": F\"{api_key}\"},\n json = json_payload\n )\n\n print(response.content)\n\nif __name__ == \"__main__\":\n main()\n" + } + ] + } + } + }, + "components": { + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-KEY", + "description": "An API key is needed to be set in the Authorization header of every API call.\nFor additional support you can contact us.\n\n- APITemplate.io expects the API key to be part of all API requests to the server in a header in this format:\n ```\n X-API-KEY: [API_KEY]\n ```\n\n- Optionally we also support Authorization header\n ```\n Authorization: Token [API_KEY]\n ```\n\n**Note: You must replace the API KEY(6fa6g2pdXGIyHRhVlGh7U56Ada1eF) with your API key in the request samples.**\n" + } + }, + "schemas": { + "Error": { + "type": "object", + "required": [ + "status", + "message" + ], + "properties": { + "status": { + "type": "string", + "description": "Value of the status: error", + "example": "error" + }, + "message": { + "type": "string", + "description": "Error message", + "example": "This is an error message" + } + } + }, + "ResponseSuccess": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Status", + "example": "success" + } + } + }, + "ResponseSuccessTemplate": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Status", + "example": "success" + }, + "template_id": { + "type": "string", + "description": "Template ID", + "example": "cd890b2b199c5c42" + }, + "body": { + "type": "string", + "description": "HTML body of the template", + "example": "

Title

\n" + }, + "css": { + "type": "string", + "description": "CSS of the template", + "example": "body{background: white}\n" + }, + "settings": { + "type": "string", + "description": "Print settings of the template", + "example": "{\"paper_size\":\"A4\",\"orientation\":\"1\",\"print_background\":\"1\",\"margin_top\":\"40\",\"margin_bottom\":\"40\",\"margin_right\":\"40\",\"margin_left\":\"40\",\"header_right\":\"{{pageNumber}}/{{totalPages}}\",\"footer_center\":\"{{pageNumber}}/{{totalPages}}\",\"header_center\":\"Sample Invoice\",\"header_font_size\":\"11px\",\"header_left\":\"{{date}}\",\"footer_left\":\"{{date}}\",\"custom_header\":\"\",\"footer_font_size\":\"11px\"}\n" + } + } + }, + "ResponseSuccessPDFFile": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Status", + "example": "success" + }, + "download_url": { + "type": "string", + "description": "Download URL", + "example": "https://bucket.s3.amazonaws.com/91f62769-69e4-48bf.pdf" + }, + "template_id": { + "type": "string", + "description": "Template ID", + "example": "cd890b2b199c5c42" + }, + "total_pages": { + "type": "integer", + "description": "Page count", + "example": 4 + }, + "transaction_ref": { + "type": "string", + "description": "Transaction reference", + "example": "a0430897-2c94-40e1-a09b-57403d811ceb" + }, + "post_actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "name": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "status": { + "type": "string" + }, + "file": { + "type": "string" + } + } + }, + "example": [ + { + "action": "S3", + "name": "S3 Storage", + "bucket": "alphacloud-test-bucket", + "status": "success", + "file": "s3://alphacloud-test-bucket/ab2e1bf7-cefa-42c7-929f-38d92b8bf8bf.pdf" + } + ] + } + } + }, + "ResponseSuccessImageFile": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Status", + "example": "success" + }, + "download_url": { + "type": "string", + "description": "Download URL", + "example": "https://bucket.s3.amazonaws.com/91f62769-69e4-48bf.jpeg" + }, + "download_url_png": { + "type": "string", + "description": "Download URL PNG", + "example": "https://bucket.s3.amazonaws.com/91f62769-69e4-48bf.png" + }, + "template_id": { + "type": "string", + "description": "Template ID", + "example": "cd890b2b199c5c42" + }, + "transaction_ref": { + "type": "string", + "description": "Transaction reference", + "example": "a0430897-2c94-40e1-a09b-57403d811ceb" + }, + "post_actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "name": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "status": { + "type": "string" + }, + "file": { + "type": "string" + } + } + }, + "example": [ + { + "action": "S3", + "name": "S3 Storage", + "bucket": "alphacloud-test-bucket", + "status": "success", + "file": "s3://alphacloud-test-bucket/91f62769-69e4-48bf.png" + }, + { + "action": "S3", + "name": "S3 Storage", + "bucket": "alphacloud-test-bucket", + "status": "success", + "file": "s3://alphacloud-test-bucket/91f62769-69e4-48bf.jpg" + } + ] + } + } + }, + "ResponseSuccessListTemplates": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success" + }, + "templates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "template_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "format": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "group_name": { + "type": "string" + } + } + }, + "example": [ + { + "template_id": "12577b29420496", + "name": "Positive Review", + "status": "ACTIVE", + "format": "JPEG", + "created_at": "2021-10-15T06:29:01.308Z", + "updated_at": "2021-10-15T13:03:43.615Z", + "group_name": "" + }, + { + "template_id": "004271e0ee394", + "name": "Test Template PDF", + "status": "ACTIVE", + "format": "PDF", + "created_at": "2021-10-09T09:57:52.224Z", + "updated_at": "2021-10-16T11:18:10.613Z", + "group_name": "" + }, + { + "template_id": "8bf77213e06b670", + "name": "New Template", + "status": "ACTIVE", + "format": "PDF", + "created_at": "2021-10-09T08:54:49.486Z", + "updated_at": "2021-10-09T09:54:44.667Z", + "group_name": "" + } + ] + } + } + }, + "ResponseSuccessListObjects": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success" + }, + "objects": { + "type": "array", + "items": { + "type": "object" + }, + "properties": { + "transaction_ref": { + "type": "string" + }, + "description": { + "type": "string" + }, + "source": { + "type": "string" + }, + "meta": { + "type": "string" + }, + "transaction_type": { + "type": "string" + }, + "primary_url": { + "type": "string" + }, + "secondary_url": { + "type": "string" + }, + "deleted_at": { + "type": "string" + }, + "deletion_status": { + "type": "integer" + }, + "ip_address": { + "type": "string" + }, + "created_at": { + "type": "string" + } + }, + "example": [ + { + "transaction_ref": "e9c46f03-1840-44dc-bae7-f280e0be98a9", + "description": null, + "source": null, + "meta": "inv-23ejh23bh", + "transaction_type": "JPEG", + "primary_url": "https://pub-cdn.apitemplate.io/e9c46f03-1840-44dc-bae7-f280e0be98a9.jpeg", + "secondary_url": "https://pub-cdn.apitemplate.io/e9c46f03-1840-44dc-bae7-f280e0be98a9.png", + "deleted_at": null, + "deletion_status": 0, + "ip_address": "1.222.242.231", + "created_at": "2021-10-16T12:08:59.281Z" + }, + { + "transaction_ref": "c973f544-fb56-465d-a1bd-35ff0e4b77e7", + "description": null, + "source": null, + "meta": "inv-45ekdjkdbh", + "transaction_type": "PDF", + "primary_url": "https://pub-cdn.apitemplate.io/2021/10/c973f544-fb56-465d-a1bd-35ff0e4b77e7.pdf", + "secondary_url": "", + "deleted_at": null, + "deletion_status": 0, + "ip_address": "1.222.242.231", + "created_at": "2021-10-16T12:07:34.478Z" + }, + { + "transaction_ref": "5ee5e0aa-4431-4d17-b94a-24ac859a5e71", + "description": null, + "source": null, + "meta": "inv-klkjbr34ded", + "transaction_type": "JPEG", + "primary_url": "https://pub-cdn.apitemplate.io/5ee5e0aa-4431-4d17-b94a-24ac859a5e71.jpeg", + "secondary_url": "https://pub-cdn.apitemplate.io/5ee5e0aa-4431-4d17-b94a-24ac859a5e71.png", + "deleted_at": null, + "deletion_status": 0, + "ip_address": "1.222.242.231", + "created_at": "2021-10-16T12:05:59.111Z" + } + ] + } + } + }, + "ResponseSuccessDeleteObject": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success" + }, + "transaction_ref": { + "example": "1618d386-2343-3d234-b9c7-99c82bb9f104" + } + } + }, + "ResponseSuccessSingleFile": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Status", + "example": "success" + }, + "primary_url": { + "type": "string", + "description": "Generated PDF document", + "example": "https://craftmypdf.com/output.pdf" + }, + "total_pages": { + "type": "integer", + "description": "Page count", + "example": 4 + }, + "transaction_ref": { + "type": "string", + "description": "Transaction reference", + "example": "a0430897-2c94-40e1-a09b-57403d811ceb" + } + } + }, + "ResponseSuccessQueryImageTemplate": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Status", + "example": "success" + }, + "width": { + "type": "integer", + "description": "Width", + "example": 1024 + }, + "height": { + "type": "integer", + "description": "Height", + "example": 1024 + }, + "layers": { + "type": "array", + "items": { + "type": "object" + }, + "description": "Array of layers", + "example": "[\n{\n \"name\": \"text_1\",\n \"type\": \"textbox\",\n \"subtype\": \"textbox\",\n \"y\": 50,\n \"x\": 50,\n \"width\": 629.82,\n \"height\": 406.8,\n \"fontSize\": 120,\n \"fontWeight\": \"normal\",\n \"fontFamily\": \"Anton\",\n \"fontStyle\": \"normal\",\n \"text\": \"Type ~something~ ::here::\",\n \"stroke\": null,\n \"strokeWidth\": 0,\n \"opacity\": 1,\n \"backgroundColor\": \"\",\n \"textAlign\": \"left\",\n \"splitByGrapheme\": false,\n \"textBackgroundColor\": \"rgba(246, 243, 243, 0)\",\n \"color\": \"#FFB029\"\n},\n{\n \"name\": \"rect_1\",\n \"type\": \"rect\",\n \"subtype\": \"rect\",\n \"y\": 101.9,\n \"x\": 708.82,\n \"width\": 300,\n \"height\": 300,\n \"stroke\": \"grey\",\n \"strokeWidth\": 3,\n \"opacity\": 1,\n \"backgroundColor\": \"\",\n \"color\": \"#BEF4FF\"\n}\n]\n" + } + } + }, + "PDFGenerationSettingsObject": { + "type": "object", + "description": "The settings object contains various properties to configure the PDF generation.\n", + "properties": { + "paper_size": { + "type": "string", + "description": "Specifies the paper size for the PDF. The available options are Letter, Legal, Tabloid, Ledger, A0, A1, A2, A3, A4, A5,A6 or custom. custom dimensions specified as \"custom_width\" and \"custom_height\".\n" + }, + "custom_width": { + "type": "string", + "description": "Custom width for the custom paper size. Valid units are mm, px and cm. eg: 30mm\n" + }, + "custom_height": { + "type": "string", + "description": "Custom height for the custom paper size. Valid units are mm, px and cm. eg: 30mm\n" + }, + "orientation": { + "type": "string", + "description": "Specifies the orientation of the PDF. The available options are \"1\" for portrait and \"2\" for landscape.\n" + }, + "header_font_size": { + "type": "string", + "description": "Specifies the font size for the header in the PDF.\n" + }, + "margin_top": { + "type": "string", + "description": "Specify the top margin for the PDF in millimeters (mm).\n" + }, + "margin_right": { + "type": "string", + "description": "Specify the right margin for the PDF in millimeters (mm).\n" + }, + "margin_bottom": { + "type": "string", + "description": "Specify the bottom margin for the PDF in millimeters (mm).\n" + }, + "margin_left": { + "type": "string", + "description": "Specify the left margin for the PDF in millimeters (mm).\n" + }, + "print_background": { + "type": "string", + "description": "Specifies whether to print the background graphics and colors in the PDF. Set to \"1\" to include backgrounds or \"0\" to exclude them.\n" + }, + "displayHeaderFooter": { + "type": "boolean", + "description": "Specifies whether to display the header and footer in the PDF. Set to true to include the header and footer or false to exclude them.\n" + }, + "custom_header": { + "type": "string", + "description": "Specify custom HTML markup for the headerof the PDF. These properties should contain valid HTML markup, including any necessary CSS styles.\n" + }, + "custom_footer": { + "type": "string", + "description": "Specify custom HTML markup for the footer of the PDF. These properties should contain valid HTML markup, including any necessary CSS styles.\n" + } + }, + "example": { + "paper_size": "A4", + "orientation": "1", + "header_font_size": "9px", + "margin_top": "40", + "margin_right": "10", + "margin_bottom": "40", + "margin_left": "10", + "print_background": "1", + "displayHeaderFooter": true, + "custom_header": "\n\n \n \n \n \n \n
", + "custom_footer": "\n\n \n \n \n \n \n
" + } + } + }, + "parameters": { + "paramTemplateID": { + "in": "query", + "name": "template_id", + "schema": { + "type": "string" + }, + "required": true, + "description": "Your template id, it can be obtained in the web console", + "example": "00377b2b1e0ee394" + }, + "paramExportType": { + "in": "query", + "name": "export_type", + "schema": { + "type": "string" + }, + "required": false, + "description": "- Either `file` or `json`(Default).\n - The option `json` returns a JSON object, and the output PDF is stored on a CDN. Use this with the parameter `expiration`\n - The option `file` returns binary data of the generated PDF(Secure and completely private) and the response HTTP header Content-Disposition is set to attachment.\n", + "example": "json" + }, + "paramExportInBase64": { + "in": "query", + "name": "export_in_base64", + "schema": { + "type": "string" + }, + "required": false, + "description": "- If export_type = `file`, the PDF can be downloaded in binary or base64 format. The value is either `1` or `0`(Default).\n - The export_in_base64 is set `0` is to download the PDF in binary\n - The export_in_base64 is set `1` is to download the PDF in base64 format\n \n", + "example": "0" + }, + "paramLoadDataFrom": { + "in": "query", + "name": "load_data_from", + "schema": { + "type": "string" + }, + "required": false, + "description": "Load JSON data from a remote URL instead of the request body. If load_data_from is specified, the JSON data in the request will be ignored.\n", + "example": "https://mydata.com/get-json-data?invoice=j3hbski2uia" + }, + "paramExpiration": { + "in": "query", + "name": "expiration", + "schema": { + "type": "integer" + }, + "required": false, + "description": "- Expiration of the generated PDF in minutes(default to `0`, store permanently)\n - Use `0` to store on cdn permanently\n - Or use the range between `1` minute and `10080` minutes(7 days) to specify the expiration of the generated PDF\n", + "example": 5 + }, + "paramOutputHTML": { + "in": "query", + "name": "output_html", + "schema": { + "type": "string" + }, + "required": false, + "description": "- Either `1` or `0`(Default).\n- To enable output of html content, set the value to `1` and it will return in the JSON response as html_url field (as a URL)\n", + "example": "0" + }, + "paramOutputFormat": { + "in": "query", + "name": "output_format", + "schema": { + "type": "string" + }, + "required": false, + "description": "- Either `pdf`(Default) or `html`.\n- It's generating PDF by default. However, you can specify output_format=html to generate only HTML(It will return in the JSON response as download_url field as a URL).\n", + "example": "pdf" + }, + "paramFileName": { + "in": "query", + "name": "filename", + "schema": { + "type": "string" + }, + "required": false, + "description": "- Default to UUID (e.g 0c93bd9e-9ebb-4634-a70f-de9131848416.pdf). Use this to specify custom file name, it should end with `.pdf`\n", + "example": "invoice_89326.pdf" + }, + "paramImageResampleRes": { + "in": "query", + "name": "image_resample_res", + "schema": { + "type": "string" + }, + "required": false, + "description": "- We embed the original images by default, meaning large PDF file sizes. Specifying the option 'image_resample_res' helps reduce the PDF file size by downsampling the images of the current PDF to a resolution(in DPI). Common values are 72, 96, 150, 300 and 600.\n", + "example": "150" + }, + "paramResizeImages": { + "in": "query", + "name": "resize_images", + "schema": { + "type": "boolean" + }, + "required": false, + "description": "- Preprocess images or re-size images in the PDF, either `1`=true or `0`=false. Default to '0'\n- If `resize_images` is set to `1`, specify the `resize_max_width`, `resize_max_height` in pixels.\n- Images to be resized need to satisfy the following conditions:\n - The images with the content-type `image/jpeg`, `image/jpg` or `image/png`\n - The image URLs with the extension `.jpg`, `.jpeg` or `.png`\n", + "example": "0" + }, + "paramResizeMaxWidth": { + "in": "query", + "name": "resize_max_width", + "schema": { + "type": "integer" + }, + "required": false, + "description": "- If `resize_images` is set to `1`, specify the maximum width of the image in pixels. Default to '1000'\n", + "example": "1000" + }, + "paramResizeMaxHeight": { + "in": "query", + "name": "resize_max_height", + "schema": { + "type": "integer" + }, + "required": false, + "description": "- If `resize_images` is set to `1`, specify the maximum height of the image in pixels. Default to '1000'\n", + "example": "1000" + }, + "paramResizeFormat": { + "in": "query", + "name": "resize_format", + "schema": { + "type": "string" + }, + "required": false, + "description": "- If `resize_images` is set to `1`, specify the format of the image. Either `jpeg` or `png`\n", + "example": "jpeg" + }, + "paramDirectDownload": { + "in": "query", + "name": "direct_download", + "schema": { + "type": "string" + }, + "required": false, + "description": "- ContentDisposition set to attachment. 1=true, 0=false. Default to '0'\n", + "example": "0" + }, + "paramCloudStorage": { + "in": "query", + "name": "cloud_storage", + "schema": { + "type": "integer" + }, + "required": false, + "description": "- Upload the generated PDFs/images to our storage CDN, default to `1`. If you have configured `Post Action` to upload the PDFs/Images to your own S3, please set it to `0`.\n", + "example": "1" + }, + "paramGenerationDelay": { + "in": "query", + "name": "generation_delay", + "schema": { + "type": "int" + }, + "required": false, + "description": "Delay in milliseconds before PDF/image generation\n" + }, + "paramPOSTACTIONS3FILEKEY": { + "in": "query", + "name": "postaction_s3_filekey", + "schema": { + "type": "string" + }, + "required": false, + "description": "- This is to specify the file name for `Post Action(AWS S3/Cloudflare R2/Azure Storage)`.\n- Please do not specify the file extension\n- Please make sure the file name is unique\n- You might use slash (/) as the folder delimiter\n- It's default to transaction_ref\n" + }, + "paramPOSTACTIONS3BUCKET": { + "in": "query", + "name": "postaction_s3_bucket", + "schema": { + "type": "string" + }, + "required": false, + "description": "- This is to overwrite the AWS Bucket for `Post Action(AWS S3/Cloudflare R2 Storage)` or the container for `Post Action(Azure Storage)`.\n" + }, + "paramMeta": { + "in": "query", + "name": "meta", + "schema": { + "type": "string" + }, + "required": false, + "description": "- Specify an external reference ID for your own reference. It appears in the `list-objects` API.\n", + "example": "inv-iwj343jospig" + }, + "paramAsync": { + "in": "query", + "name": "async", + "schema": { + "type": "string" + }, + "required": false, + "description": "- Either `1` or `0`(Default). `0` is synchronous call(default), `1` is asynchronous call\n- To generate PDF asynchronously, set the value to `1` and the API call returns immediately. Once the PDF document is generated, we will make a HTTP/HTTPS GET to your URL(webhook_url) and will retry for 3 times before giving up.\n- If `async` is set to `1`, then `webhook_url` is mandatory\n", + "example": "0" + }, + "paramWebhook": { + "in": "query", + "name": "webhook_url", + "schema": { + "type": "string" + }, + "required": false, + "description": "- It is the URL of your webhook URL, it starts with http:// or https:// and has to be urlencoded.\n- If `async` is set to `1`, then you have to specify the `webhook_url`.\n\n\n#### Format of Webhook callback\n\nOnce the PDF is generated, we will initiate a HTTP/HTTPS GET call to the following URL:\n\nhttps://`[yourwebserver.com]`?&primary_url=`[primary_url]`&transaction_ref=`[transaction_ref]`&status=`[status]`&message=`[message]`\n\n- `[yourwebserver.com]`: The web services to handle the callback, which is the `webhook_url`\n- `[primary_url]`: The URL to the PDF document\n- `[transaction_ref]`: The transaction reference number\n- `[status]` : Status of the transaction, either `success` or `error`\n- `[message]` : Status message\n\n***The following is a sample webhook call back to your server***\n\nhttps://yourwebserver.com?&primary_url=https%3A%2F%2Fpub-cdn.apitemplate.io%2F2021%2F06%2Fb692183d-46d7-3213-891a-460a5814ad3f.pdf&transaction_ref=b692183d-46d7-3213-891a-460a5814ad3f&status=success\n", + "example": "https://yourwebserver.com" + }, + "paramWebhookMethod": { + "in": "query", + "name": "webhook_method", + "schema": { + "type": "string" + }, + "required": false, + "description": "- The HTTP method of the webhook, either `POST` or `GET`. Default to `GET`\n", + "example": "GET" + }, + "paramWebhookHeaders": { + "in": "query", + "name": "webhook_headers", + "schema": { + "type": "string" + }, + "required": false, + "description": "- The HTTP headers of the webhook, it should be a base64 encoded JSON object.\n- The following is an example of base64 encoded JSON:\n ```json\n eyJ3b3JrZmxvdy1hcGkta2V5Ijoia2V5X0VLc3MxNWJKRXFBMkRHYzM4bkNXNzlaRER1ZUZJeiJ9\n ```\n\n The JSON object in clear text for the above base64 encoded JSON:\n ```json\n { \n \"workflow-api-key\": \"key_EKss15bJEqA2DGc38nCW79ZDDueFIz\"\n }\n ```\n", + "example": "eyJ3b3JrZmxvdy1hcGkta2V5Ijoia2V5X0VLc3MxNWJKRXFBMkRHYzM4bkNXNzlaRER1ZUZJeiJ9" + } + } + } +} \ No newline at end of file diff --git a/server/node-service/src/plugins/apiTemplate/index.ts b/server/node-service/src/plugins/apiTemplate/index.ts new file mode 100644 index 0000000000..8b79208201 --- /dev/null +++ b/server/node-service/src/plugins/apiTemplate/index.ts @@ -0,0 +1,65 @@ +import { readYaml } from "../../common/util"; +import _ from "lodash"; +import path from "path"; +import { OpenAPIV3, OpenAPI } from "openapi-types"; +import { ConfigToType, DataSourcePlugin } from "lowcoder-sdk/dataSource"; +import { runOpenApi } from "../openApi"; +import { parseOpenApi, ParseOpenApiOptions } from "../openApi/parse"; + +import spec from './apiTemplate.spec.json'; + +const dataSourceConfig = { + type: "dataSource", + params: [ + { + "type": "groupTitle", + "key": "ApiKeyAuth", + "label": "Api Key Auth" + }, + { + "type": "password", + "key": "ApiKeyAuth.value", + "label": "X-API-KEY", + "tooltip": "An API key is needed to be set in the Authorization header of every API call.\nFor additional support you can contact us.\n\n- APITemplate.io expects the API key to be part of all API requests to the server in a header in this format:\n ```\n X-API-KEY: [API_KEY]\n ```\n\n- Optionally we also support Authorization header\n ```\n Authorization: Token [API_KEY]\n ```\n\n**Note: You must replace the API KEY(6fa6g2pdXGIyHRhVlGh7U56Ada1eF) with your API key in the request samples.**\n", + "placeholder": "An API key is needed to be set in the Authorization header of every API call.\nFor additional support you can contact us.\n\n- APITemplate.io expects the API key to be part of all API requests to the server in a header in this format:\n ```\n X-API-KEY: [API_KEY]\n ```\n\n- Optionally we also support Authorization header\n ```\n Authorization: Token [API_KEY]\n ```\n\n**Note: You must replace the API KEY(6fa6g2pdXGIyHRhVlGh7U56Ada1eF) with your API key in the request samples.**\n" + } +] +} as const; + +const parseOptions: ParseOpenApiOptions = { + actionLabel: (method: string, path: string, operation: OpenAPI.Operation) => { + return _.upperFirst(operation.operationId || ""); + }, +}; + +type DataSourceConfigType = ConfigToType; + +const apiTemplatePlugin: DataSourcePlugin = { + id: "apiTemplate", + name: "ApiTemplate", + icon: "apiTemplate.svg", + category: "Assets", + dataSourceConfig, + queryConfig: async () => { + const { actions, categories } = await parseOpenApi(spec as unknown as OpenAPI.Document, parseOptions); + return { + type: "query", + label: "Action", + categories: { + label: "Resources", + items: categories, + }, + actions, + }; + }, + run: function (actionData, dataSourceConfig): Promise { + const runApiDsConfig = { + url: "", + serverURL: "", + dynamicParamsConfig: dataSourceConfig, + }; + return runOpenApi(actionData, runApiDsConfig, spec as OpenAPIV3.Document); + }, +}; + +export default apiTemplatePlugin; diff --git a/server/node-service/src/plugins/index.ts b/server/node-service/src/plugins/index.ts index a49e1c2a37..8133e63386 100644 --- a/server/node-service/src/plugins/index.ts +++ b/server/node-service/src/plugins/index.ts @@ -39,6 +39,7 @@ import postmanEchoPlugin from "./postmanEcho"; import lowcoderPlugin from "./lowcoder"; import supabaseApiPlugin from "./supabaseApi"; import firebirdsqlPlugin from "./firebirdsql"; +import apiTemplatePlugin from "./apiTemplate"; // import boomiPlugin from "./boomi"; let plugins: (DataSourcePlugin | DataSourcePluginFactory)[] = [ @@ -90,6 +91,7 @@ let plugins: (DataSourcePlugin | DataSourcePluginFactory)[] = [ googleCloudStorage, supabasePlugin, cloudinaryPlugin, + apiTemplatePlugin, ossPlugin, // Project Management @@ -112,4 +114,4 @@ try { console.info("using ee plugins"); } catch { } -export default plugins; +export default plugins; \ No newline at end of file diff --git a/server/node-service/src/services/plugin.ts b/server/node-service/src/services/plugin.ts index 2dbe984f26..f7376fee5f 100644 --- a/server/node-service/src/services/plugin.ts +++ b/server/node-service/src/services/plugin.ts @@ -189,7 +189,7 @@ export function listPlugins(ctx: PluginContext, ids: string[] = []) { const pluginMeta = { ...plugin, shouldValidateDataSourceConfig: !!plugin.validateDataSourceConfig, - } as DataSourcePluginMeta; + } as unknown as DataSourcePluginMeta; pluginMetaOps.forEach(([path, fn]) => { jsonPath.apply(pluginMeta, path, fn); diff --git a/server/node-service/src/static/plugin-icons/apiTemplate.svg b/server/node-service/src/static/plugin-icons/apiTemplate.svg new file mode 100644 index 0000000000..8bac8a1583 --- /dev/null +++ b/server/node-service/src/static/plugin-icons/apiTemplate.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/server/node-service/yarn.lock b/server/node-service/yarn.lock index 03d63ef0f4..0a492dcd85 100644 --- a/server/node-service/yarn.lock +++ b/server/node-service/yarn.lock @@ -7689,16 +7689,16 @@ __metadata: languageName: node linkType: hard -"lowcoder-core@npm:^0.0.8": - version: 0.0.8 - resolution: "lowcoder-core@npm:0.0.8" +"lowcoder-core@npm:^0.0.10": + version: 0.0.10 + resolution: "lowcoder-core@npm:0.0.10" dependencies: "@rollup/plugin-commonjs": ^23.0.0 "@rollup/plugin-node-resolve": ^15.0.0 intl-messageformat: ^10.2.1 lodash: ^4.17.21 lru-cache: ^7.14.1 - checksum: 67f6ddc1b924d96d5d2ba0fca05b50bf91035b3d24d51acf89e0e40ca466121ba0f220f7162215b71077a43025b9df0a9b55aee48a937605a73fec4a06b71cac + checksum: f41ae738c8c46df132d8bb31a749e4aa0542e087302cefe078b55cb503372c9979e2e97c926d3ca1592de3aad64a70e8dab2454458b593f1f983f27ad4f85708 languageName: node linkType: hard @@ -7743,8 +7743,8 @@ __metadata: jsonpath: ^1.1.1 lodash: ^4.17.21 loglevel: ^1.8.1 - lowcoder-core: ^0.0.8 - lowcoder-sdk: 0.0.41 + lowcoder-core: ^0.0.10 + lowcoder-sdk: 2.4.17 morgan: ^1.10.0 nock: ^13.3.0 node-fetch: 2 @@ -7765,13 +7765,15 @@ __metadata: languageName: unknown linkType: soft -"lowcoder-sdk@npm:0.0.41": - version: 0.0.41 - resolution: "lowcoder-sdk@npm:0.0.41" +"lowcoder-sdk@npm:2.4.17": + version: 2.4.17 + resolution: "lowcoder-sdk@npm:2.4.17" + dependencies: + prettier: ^3.1.1 peerDependencies: - react: ">=17" - react-dom: ">=17" - checksum: f7820b8ddfc9e86c3c36923347a686325b449a9d01cad761c0800e27d6f3408e76668664a24667eeb19eb6674f5024113da7a9fc3881cf5ce28d6f9304444c79 + react: ">=18" + react-dom: ">=18" + checksum: d4ef5af5e90070aa55b04a190c6b4ad24a28101836db30b21629ff0a3e2428b0daf29b1670a4a44418cd58d18384ef8d19d3327d9f057c459b560f0c357b675b languageName: node linkType: hard @@ -8422,8 +8424,8 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 10.2.0 - resolution: "node-gyp@npm:10.2.0" + version: 10.3.1 + resolution: "node-gyp@npm:10.3.1" dependencies: env-paths: ^2.2.0 exponential-backoff: ^3.1.1 @@ -8437,7 +8439,7 @@ __metadata: which: ^4.0.0 bin: node-gyp: bin/node-gyp.js - checksum: 0233759d8c19765f7fdc259a35eb046ad86c3d09e22f7384613ae2b89647dd27fcf833fdf5293d9335041e91f9b1c539494225959cdb312a5c8080b7534b926f + checksum: 91b0690ab504fe051ad66863226dc5ecac72b8471f85e8428e4d5ca3217d3a2adfffae48cd555e8d009a4164689fff558b88d2bc9bfd246452a3336ab308cf99 languageName: node linkType: hard @@ -8984,6 +8986,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:^3.1.1": + version: 3.4.2 + resolution: "prettier@npm:3.4.2" + bin: + prettier: bin/prettier.cjs + checksum: 061c84513db62d3944c8dc8df36584dad82883ce4e49efcdbedd8703dce5b173c33fd9d2a4e1725d642a3b713c932b55418342eaa347479bc4a9cca114a04cd0 + languageName: node + linkType: hard + "pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0"