From ebfbdfdfd794e58923ee1952fd9593bfb5bc7708 Mon Sep 17 00:00:00 2001 From: freddysundowner Date: Thu, 22 Feb 2024 14:05:36 +0300 Subject: [PATCH 01/32] added licence key data field --- client/packages/lowcoder-comps/package.json | 4 +- .../src/comps/calendarComp/calendarComp.tsx | 85 ++++-- .../comps/calendarComp/calendarConstants.tsx | 8 +- .../src/i18n/comps/locales/en.ts | 287 +++++++++--------- 4 files changed, 219 insertions(+), 165 deletions(-) diff --git a/client/packages/lowcoder-comps/package.json b/client/packages/lowcoder-comps/package.json index 1bf1dae92..d1f7f0a91 100644 --- a/client/packages/lowcoder-comps/package.json +++ b/client/packages/lowcoder-comps/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-comps", - "version": "0.0.24", + "version": "0.0.25", "type": "module", "license": "MIT", "dependencies": { @@ -10,6 +10,8 @@ "@fullcalendar/list": "^6.1.9", "@fullcalendar/moment": "^6.1.6", "@fullcalendar/react": "^6.1.6", + "@fullcalendar/resource": "^6.1.11", + "@fullcalendar/resource-timeline": "^6.1.11", "@fullcalendar/timegrid": "^6.1.6", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", diff --git a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx index 72f292ece..320076851 100644 --- a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx @@ -26,6 +26,7 @@ import { default as Form } from "antd/es/form"; import { default as Input } from "antd/es/input"; import { trans, getCalendarLocale } from "../../i18n/comps"; import { createRef, useContext, useRef, useState } from "react"; +import resourceTimelinePlugin from "@fullcalendar/resource-timeline"; import FullCalendar from "@fullcalendar/react"; import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; @@ -66,8 +67,9 @@ const childrenMap = { dayMaxEvents: withDefault(NumberControl, 2), eventMaxStack: withDefault(NumberControl, 0), style: styleControl(CalendarStyle), + licenceKey: withDefault(StringControl, ""), }; - + let CalendarBasicComp = (function () { return new UICompBuilder(childrenMap, (props) => { const theme = useContext(ThemeContext); @@ -83,7 +85,9 @@ let CalendarBasicComp = (function () { start: dayjs(item.start, DateParser).format(), end: dayjs(item.end, DateParser).format(), allDay: item.allDay, - color: isValidColor(item.color || "") ? item.color : theme?.theme?.primary, + color: isValidColor(item.color || "") + ? item.color + : theme?.theme?.primary, ...(item.groupId ? { groupId: item.groupId } : null), }; }); @@ -104,8 +108,13 @@ let CalendarBasicComp = (function () { function renderEventContent(eventInfo: EventContentArg) { const isList = eventInfo.view.type === "listWeek"; let sizeClass = ""; - if ([ViewType.WEEK, ViewType.DAY].includes(eventInfo.view.type as ViewType)) { - const duration = dayjs(eventInfo.event.end).diff(dayjs(eventInfo.event.start), "minutes"); + if ( + [ViewType.WEEK, ViewType.DAY].includes(eventInfo.view.type as ViewType) + ) { + const duration = dayjs(eventInfo.event.end).diff( + dayjs(eventInfo.event.start), + "minutes" + ); if (duration <= 30 || eventInfo.event.allDay) { sizeClass = "small"; } else if (duration <= 60) { @@ -137,7 +146,9 @@ let CalendarBasicComp = (function () { onClick={(e) => { e.stopPropagation(); props.onEvent("change"); - const event = events.filter((item: EventType) => item.id !== eventInfo.event.id); + const event = events.filter( + (item: EventType) => item.id !== eventInfo.event.id + ); props.events.onChange(event); }} onMouseDown={(e) => { @@ -195,7 +206,9 @@ let CalendarBasicComp = (function () { }; const showModal = (event: EventType, ifEdit: boolean) => { - const modalTitle = ifEdit ? trans("calendar.editEvent") : trans("calendar.creatEvent"); + const modalTitle = ifEdit + ? trans("calendar.editEvent") + : trans("calendar.creatEvent"); form && form.setFieldsValue(event); const eventId = editEvent.current?.id; CustomModal.confirm({ @@ -209,14 +222,18 @@ let CalendarBasicComp = (function () { } name="id" - rules={[{ required: true, message: trans("calendar.eventIdRequire") }]} + rules={[ + { required: true, message: trans("calendar.eventIdRequire") }, + ]} > @@ -239,9 +256,13 @@ let CalendarBasicComp = (function () { form.submit(); return form.validateFields().then(() => { const { id, groupId, color, title = "" } = form.getFieldsValue(); - const idExist = props.events.value.findIndex((item: EventType) => item.id === id); + const idExist = props.events.value.findIndex( + (item: EventType) => item.id === id + ); if (idExist > -1 && id !== eventId) { - form.setFields([{ name: "id", errors: [trans("calendar.eventIdExist")] }]); + form.setFields([ + { name: "id", errors: [trans("calendar.eventIdExist")] }, + ]); throw new Error(); } if (ifEdit) { @@ -306,7 +327,14 @@ let CalendarBasicComp = (function () { locale={getCalendarLocale()} locales={allLocales} firstDay={Number(firstDay)} - plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin, momentPlugin]} + plugins={[ + dayGridPlugin, + timeGridPlugin, + interactionPlugin, + listPlugin, + momentPlugin, + resourceTimelinePlugin, + ]} headerToolbar={headerToolbar} moreLinkClick={(info) => { let left = 0; @@ -319,15 +347,19 @@ let CalendarBasicComp = (function () { } } else { if (info.allDay) { - left = ele.offsetParent?.parentElement?.parentElement?.offsetLeft || 0; + left = + ele.offsetParent?.parentElement?.parentElement?.offsetLeft || + 0; } else { left = - ele.offsetParent?.parentElement?.parentElement?.parentElement?.offsetLeft || 0; + ele.offsetParent?.parentElement?.parentElement?.parentElement + ?.offsetLeft || 0; } } setLeft(left); }} buttonText={buttonText} + schedulerLicenseKey={props.licenceKey.value} views={views} eventClassNames={() => (!showEventTime ? "no-time" : "")} slotLabelFormat={slotLabelFormat} @@ -346,7 +378,9 @@ let CalendarBasicComp = (function () { eventContent={renderEventContent} select={(info) => handleCreate(info)} eventClick={(info) => { - const event = events.find((item: EventType) => item.id === info.event.id); + const event = events.find( + (item: EventType) => item.id === info.event.id + ); editEvent.current = event; setTimeout(() => { editEvent.current = undefined; @@ -387,12 +421,19 @@ let CalendarBasicComp = (function () { .setPropertyViewFn((children) => { return ( <> -
{children.events.propertyView({})}
-
{children.onEvent.getPropertyView()}
+
+ {children.events.propertyView({})} +
+
+ {children.licenceKey.propertyView({ + label: trans("calendar.licence"), + })} + {children.onEvent.getPropertyView()} +
{children.editable.propertyView({ - label: trans("calendar.editable"), - })} + label: trans("calendar.editable"), + })} {children.defaultDate.propertyView({ label: trans("calendar.defaultDate"), tooltip: trans("calendar.defaultDateTooltip"), @@ -424,8 +465,12 @@ let CalendarBasicComp = (function () { tooltip: trans("calendar.eventMaxStackTooltip"), })}
-
{hiddenPropertyView(children)}
-
{children.style.getPropertyView()}
+
+ {hiddenPropertyView(children)} +
+
+ {children.style.getPropertyView()} +
); }) diff --git a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarConstants.tsx b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarConstants.tsx index 93c07c462..37cca2c1c 100644 --- a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarConstants.tsx +++ b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarConstants.tsx @@ -205,9 +205,9 @@ export const Wrapper = styled.div<{ flex-direction: inherit; } .fc-day-today .fc-daygrid-day-number { - background-color: ${(props) => props.$theme.primary}; + background-color: ${(props) => props.$theme?.primary}; color: ${(props) => - contrastText(props.$theme.primary || "", props.$theme.textDark, props.$theme.textLight)}; + contrastText(props.$theme?.primary || "", props.$theme?.textDark, props.$theme?.textLight)}; } .fc-daygrid-day-events { padding: 1px 0 5px 0; @@ -585,10 +585,10 @@ export const Wrapper = styled.div<{ } .fc-day-today.fc-col-header-cell { background-color: ${(props) => - isDarkColor(props.$style.background) ? "#ffffff19" : toHex(props.$theme.primary!) + "19"}; + isDarkColor(props.$style.background) ? "#ffffff19" : toHex(props.$theme?.primary!) + "19"}; a { color: ${(props) => - !isDarkColor(props.$style.background) && darkenColor(props.$theme.primary!, 0.1)}; + !isDarkColor(props.$style.background) && darkenColor(props.$theme?.primary!, 0.1)}; } } .fc-col-header-cell-cushion { 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 9cac1339b..048650e24 100644 --- a/client/packages/lowcoder-comps/src/i18n/comps/locales/en.ts +++ b/client/packages/lowcoder-comps/src/i18n/comps/locales/en.ts @@ -1,145 +1,152 @@ export const en = { - "chart": { - "delete": "Delete", - "data": "Data", - "mode": "Mode", - "config": "Configuration", - "UIMode": "UI Mode", - "chartType": "Chart Type", - "xAxis": "X-axis", - "chartSeries": "Chart Series", - "customSeries": "Custom Series", - "add": "Add", - "confirmDelete": "Confirm Delete: ", - "seriesName": "Series Name", - "dataColumns": "Data Columns", - "title": "Title", - "xAxisDirection": "X-axis Direction", - "xAxisName": "X-axis Name", - "xAxisType": "X-axis Type", - "xAxisTypeTooltip": "Automatically detected based on X-axis data. For type description, refer to: ", - "logBase": "Log Base", - "yAxisName": "Y-axis Name", - "yAxisType": "Y-axis Type", - "yAxisDataFormat": "Y-axis Data Type", - "yAxisDataFormatTooltip": "Indicates the value of each coordinate. Example: '{{value * 100 + \"%\"}}'", - "basicBar": "Basic Bar", - "stackedBar": "Stacked Bar", - "barType": "Bar Chart Type", - "categoryAxis": "Category Axis", - "valueAxis": "Value Axis", - "timeAxis": "Time Axis", - "logAxis": "Log Axis", - "auto": "Default", - "legendPosition": "Legend Position", - "basicLine": "Basic Line", - "stackedLine": "Stacked Line", - "areaLine": "Area Line", - "smooth": "Smooth Curve", - "lineType": "Line Chart Type", - "basicPie": "Basic Pie", - "doughnutPie": "Doughnut Pie", - "rosePie": "Rose Pie", - "pieType": "Pie Chart Type", - "spending": "Spending", - "budget": "Budget", - "bar": "Bar Chart", - "line": "Line Chart", - "scatter": "Scatter Chart", - "pie": "Pie Chart", - "horizontal": "Horizontal", - "vertical": "Vertical", - "noData": "No Data", - "unknown": "Unknown", - "select": "Select", - "unSelect": "Unselect", - "echartsOptionLabel": "Option", - "echartsOptionTooltip": "ECharts Option", - "echartsOptionExamples": "ECharts Examples", - "echartsMapOptionTooltip": "ECharts Map Option", - "echartsMapOptionExamples": "ECharts Map Examples", - "selectDesc": "Triggered when a user selects part of the data in the chart", - "unselectDesc": "Triggered when a user unselects part of the data in the chart", - "selectedPointsDesc": "Selected Points", - "dataDesc": "JSON Data for the Chart", - "titleDesc": "Current Chart Title", - "scatterShape": "Scatter Shape", - "circle": "Circle", - "rect": "Rectangle", - "triangle": "Triangle", - "diamond": "Diamond", - "pin": "Pin", - "arrow": "Arrow", - "pointColorLabel": "Point Color", - "pointColorTooltip": "Set point color based on series name and value. Variables: seriesName, value. Example: '{{value < 25000 ? \"red\" : \"green\"}}'", - "mapReady": "Map Ready", - "mapReadyDesc": "Triggers when the map is ready", - "zoomLevelChange": "Zoom Level Change", - "zoomLevelChangeDesc": "Triggers when the map zoom level changes", - "centerPositionChange": "Center Position Change", - "centerPositionChangeDesc": "Triggers when the map center position changes" + chart: { + delete: "Delete", + data: "Data", + mode: "Mode", + config: "Configuration", + UIMode: "UI Mode", + chartType: "Chart Type", + xAxis: "X-axis", + chartSeries: "Chart Series", + customSeries: "Custom Series", + add: "Add", + confirmDelete: "Confirm Delete: ", + seriesName: "Series Name", + dataColumns: "Data Columns", + title: "Title", + xAxisDirection: "X-axis Direction", + xAxisName: "X-axis Name", + xAxisType: "X-axis Type", + xAxisTypeTooltip: + "Automatically detected based on X-axis data. For type description, refer to: ", + logBase: "Log Base", + yAxisName: "Y-axis Name", + yAxisType: "Y-axis Type", + yAxisDataFormat: "Y-axis Data Type", + yAxisDataFormatTooltip: + "Indicates the value of each coordinate. Example: '{{value * 100 + \"%\"}}'", + basicBar: "Basic Bar", + stackedBar: "Stacked Bar", + barType: "Bar Chart Type", + categoryAxis: "Category Axis", + valueAxis: "Value Axis", + timeAxis: "Time Axis", + logAxis: "Log Axis", + auto: "Default", + legendPosition: "Legend Position", + basicLine: "Basic Line", + stackedLine: "Stacked Line", + areaLine: "Area Line", + smooth: "Smooth Curve", + lineType: "Line Chart Type", + basicPie: "Basic Pie", + doughnutPie: "Doughnut Pie", + rosePie: "Rose Pie", + pieType: "Pie Chart Type", + spending: "Spending", + budget: "Budget", + bar: "Bar Chart", + line: "Line Chart", + scatter: "Scatter Chart", + pie: "Pie Chart", + horizontal: "Horizontal", + vertical: "Vertical", + noData: "No Data", + unknown: "Unknown", + select: "Select", + unSelect: "Unselect", + echartsOptionLabel: "Option", + echartsOptionTooltip: "ECharts Option", + echartsOptionExamples: "ECharts Examples", + echartsMapOptionTooltip: "ECharts Map Option", + echartsMapOptionExamples: "ECharts Map Examples", + selectDesc: "Triggered when a user selects part of the data in the chart", + unselectDesc: + "Triggered when a user unselects part of the data in the chart", + selectedPointsDesc: "Selected Points", + dataDesc: "JSON Data for the Chart", + titleDesc: "Current Chart Title", + scatterShape: "Scatter Shape", + circle: "Circle", + rect: "Rectangle", + triangle: "Triangle", + diamond: "Diamond", + pin: "Pin", + arrow: "Arrow", + pointColorLabel: "Point Color", + pointColorTooltip: + 'Set point color based on series name and value. Variables: seriesName, value. Example: \'{{value < 25000 ? "red" : "green"}}\'', + mapReady: "Map Ready", + mapReadyDesc: "Triggers when the map is ready", + zoomLevelChange: "Zoom Level Change", + zoomLevelChangeDesc: "Triggers when the map zoom level changes", + centerPositionChange: "Center Position Change", + centerPositionChangeDesc: "Triggers when the map center position changes", }, - "imageEditor": { - "defaultSrc": "", - "save": "Save", - "saveDesc": "Save Image", - "src": "Image Source", - "name": "Image Name", - "buttonText": "Button Text", - "srcDesc": "Image Source", - "nameDesc": "Image Name", - "dataURIDesc": "Image Data URI", - "dataDesc": "Image Data", - "buttonTextDesc": "Button Text" + imageEditor: { + defaultSrc: "", + save: "Save", + saveDesc: "Save Image", + src: "Image Source", + name: "Image Name", + buttonText: "Button Text", + srcDesc: "Image Source", + nameDesc: "Image Name", + dataURIDesc: "Image Data URI", + dataDesc: "Image Data", + buttonTextDesc: "Button Text", }, - "calendar": { - "events": "Events Data", - "editable": "Editable", - "defaultDate": "Default Date", - "defaultDateTooltip": "Initial display date of the calendar", - "defaultView": "Default View", - "defaultViewTooltip": "Initial view of the calendar", - "showEventTime": "Show Event Times", - "showEventTimeTooltip": "Display event time text", - "showWeekends": "Show Weekends", - "showAllDay": "Show All-Day", - "showAllDayTooltip": "Display all-day slot in week and day views", - "dayMaxEvents": "Day Max Events", - "dayMaxEventsTooltip": "Max events per day in month view, 0 for cell height limit", - "eventMaxStack": "Event Max Stack", - "eventMaxStackTooltip": "Max events to stack horizontally in week and day views, 0 for no limit", - "selectInterval": "Selected Interval", - "selectEvent": "Selected Event", - "changeSet": "Changed Event Object", - "headerBtnBackground": "Button Background", - "btnText": "Button Text", - "title": "Title", - "selectBackground": "Selected Background", - "today": "Today", - "month": "Month", - "week": "Week", - "day": "Day", - "list": "List", - "monday": "Monday", - "tuesday": "Tuesday", - "wednesday": "Wednesday", - "thursday": "Thursday", - "friday": "Friday", - "saturday": "Saturday", - "sunday": "Sunday", - "startWeek": "Start From", - "creatEvent": "Create Event", - "editEvent": "Edit Event", - "eventName": "Event Name", - "eventColor": "Event Color", - "eventGroupId": "Group ID", - "groupIdTooltip": "Group ID groups events for drag and resize together.", - "more": "More", - "allDay": "All Day", - "eventNameRequire": "Enter Event Name", - "eventId": "Event ID", - "eventIdRequire": "Enter Event ID", - "eventIdTooltip": "Unique ID for each event", - "eventIdExist": "ID Exists" + calendar: { + events: "Events Data", + editable: "Editable", + licence: "Licence Key", + defaultDate: "Default Date", + defaultDateTooltip: "Initial display date of the calendar", + defaultView: "Default View", + defaultViewTooltip: "Initial view of the calendar", + showEventTime: "Show Event Times", + showEventTimeTooltip: "Display event time text", + showWeekends: "Show Weekends", + showAllDay: "Show All-Day", + showAllDayTooltip: "Display all-day slot in week and day views", + dayMaxEvents: "Day Max Events", + dayMaxEventsTooltip: + "Max events per day in month view, 0 for cell height limit", + eventMaxStack: "Event Max Stack", + eventMaxStackTooltip: + "Max events to stack horizontally in week and day views, 0 for no limit", + selectInterval: "Selected Interval", + selectEvent: "Selected Event", + changeSet: "Changed Event Object", + headerBtnBackground: "Button Background", + btnText: "Button Text", + title: "Title", + selectBackground: "Selected Background", + today: "Today", + month: "Month", + week: "Week", + day: "Day", + list: "List", + monday: "Monday", + tuesday: "Tuesday", + wednesday: "Wednesday", + thursday: "Thursday", + friday: "Friday", + saturday: "Saturday", + sunday: "Sunday", + startWeek: "Start From", + creatEvent: "Create Event", + editEvent: "Edit Event", + eventName: "Event Name", + eventColor: "Event Color", + eventGroupId: "Group ID", + groupIdTooltip: "Group ID groups events for drag and resize together.", + more: "More", + allDay: "All Day", + eventNameRequire: "Enter Event Name", + eventId: "Event ID", + eventIdRequire: "Enter Event ID", + eventIdTooltip: "Unique ID for each event", + eventIdExist: "ID Exists", }, }; From df156a58fa17f1275f3c38b443100c4f6505de60 Mon Sep 17 00:00:00 2001 From: freddysundowner Date: Fri, 23 Feb 2024 13:04:51 +0300 Subject: [PATCH 02/32] fixed access of licence in the full calendar --- .../lowcoder-comps/src/comps/calendarComp/calendarComp.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx index 320076851..5cb817e31 100644 --- a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx @@ -103,6 +103,7 @@ let CalendarBasicComp = (function () { style, firstDay, editable, + licenceKey, } = props; function renderEventContent(eventInfo: EventContentArg) { @@ -309,6 +310,8 @@ let CalendarBasicComp = (function () { initialDate = undefined; } + + return ( (!showEventTime ? "no-time" : "")} slotLabelFormat={slotLabelFormat} From 0e95abd0c16f3529e225089336a104335bfaf2e8 Mon Sep 17 00:00:00 2001 From: freddysundowner Date: Fri, 23 Feb 2024 13:05:29 +0300 Subject: [PATCH 03/32] fixed bug on licence key input filled --- .../src/comps/comps/remoteComp/loaders.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx b/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx index 24038fb45..93011ee73 100644 --- a/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx +++ b/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx @@ -1,11 +1,18 @@ import { NPM_PLUGIN_ASSETS_BASE_URL } from "constants/npmPlugins"; import { trans } from "i18n"; import { CompConstructor } from "lowcoder-core"; -import { RemoteCompInfo, RemoteCompLoader, RemoteCompSource } from "types/remoteComp"; +import { + RemoteCompInfo, + RemoteCompLoader, + RemoteCompSource, +} from "types/remoteComp"; -async function npmLoader(remoteInfo: RemoteCompInfo): Promise { +async function npmLoader( + remoteInfo: RemoteCompInfo +): Promise { const { packageName, packageVersion = "latest", compName } = remoteInfo; const entry = `${NPM_PLUGIN_ASSETS_BASE_URL}/${packageName}@${packageVersion}/index.js`; + // const entry = `../../../../../public/package/index.js`; // console.log("Entry", entry); try { const module = await import(/* webpackIgnore: true */ entry); @@ -21,7 +28,9 @@ async function npmLoader(remoteInfo: RemoteCompInfo): Promise { +async function bundleLoader( + remoteInfo: RemoteCompInfo +): Promise { const { packageName, packageVersion = "latest", compName } = remoteInfo; const entry = `/${packageName}/${packageVersion}/index.js?v=${REACT_APP_COMMIT_ID}`; const module = await import(/* webpackIgnore: true */ entry); From a1918e511c12a8f83f6481594a44f5ab2643bbcb Mon Sep 17 00:00:00 2001 From: freddysundowner Date: Mon, 26 Feb 2024 16:00:19 +0300 Subject: [PATCH 04/32] "add premium and free calendar options drop down" --- client/packages/lowcoder-comps/package.json | 2 + .../src/comps/calendarComp/calendarComp.tsx | 48 +++++-- .../comps/calendarComp/calendarConstants.tsx | 92 ++++++++++---- .../src/i18n/comps/locales/en.ts | 1 + client/yarn.lock | 119 ++++++++++++++++++ 5 files changed, 227 insertions(+), 35 deletions(-) diff --git a/client/packages/lowcoder-comps/package.json b/client/packages/lowcoder-comps/package.json index d67a915cd..fe4d32f98 100644 --- a/client/packages/lowcoder-comps/package.json +++ b/client/packages/lowcoder-comps/package.json @@ -4,6 +4,7 @@ "type": "module", "license": "MIT", "dependencies": { + "@fullcalendar/adaptive": "^6.1.11", "@fullcalendar/core": "^6.1.6", "@fullcalendar/daygrid": "^6.1.6", "@fullcalendar/interaction": "^6.1.6", @@ -11,6 +12,7 @@ "@fullcalendar/moment": "^6.1.6", "@fullcalendar/react": "^6.1.6", "@fullcalendar/resource": "^6.1.11", + "@fullcalendar/resource-timegrid": "^6.1.11", "@fullcalendar/resource-timeline": "^6.1.11", "@fullcalendar/timegrid": "^6.1.6", "@types/react": "^18.2.45", diff --git a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx index 5cb817e31..195e5c1fb 100644 --- a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx @@ -27,6 +27,9 @@ import { default as Input } from "antd/es/input"; import { trans, getCalendarLocale } from "../../i18n/comps"; import { createRef, useContext, useRef, useState } from "react"; import resourceTimelinePlugin from "@fullcalendar/resource-timeline"; +import resourceTimeGridPlugin from "@fullcalendar/resource-timegrid"; +import adaptivePlugin from "@fullcalendar/adaptive"; + import FullCalendar from "@fullcalendar/react"; import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; @@ -36,7 +39,8 @@ import allLocales from "@fullcalendar/core/locales-all"; import { EventContentArg, DateSelectArg } from "@fullcalendar/core"; import momentPlugin from "@fullcalendar/moment"; import { - DefaultViewOptions, + DefaultWithFreeViewOptions, + DefaultWithPremiumViewOptions, FirstDayOptions, Wrapper, Event, @@ -53,13 +57,20 @@ import { } from "./calendarConstants"; import dayjs from "dayjs"; +function filterViews() {} + const childrenMap = { events: jsonValueExposingStateControl("events", defaultData), onEvent: ChangeEventHandlerControl, editable: withDefault(BoolControl, true), defaultDate: withDefault(StringControl, "{{ new Date() }}"), - defaultView: dropdownControl(DefaultViewOptions, "timeGridWeek"), + defaultFreeView: dropdownControl(DefaultWithFreeViewOptions, "timeGridWeek"), + defaultPremiumView: dropdownControl( + DefaultWithPremiumViewOptions, + "timeGridWeek" + ), + firstDay: dropdownControl(FirstDayOptions, "1"), showEventTime: withDefault(BoolControl, true), showWeekends: withDefault(BoolControl, true), @@ -69,7 +80,7 @@ const childrenMap = { style: styleControl(CalendarStyle), licenceKey: withDefault(StringControl, ""), }; - + let CalendarBasicComp = (function () { return new UICompBuilder(childrenMap, (props) => { const theme = useContext(ThemeContext); @@ -94,7 +105,8 @@ let CalendarBasicComp = (function () { const { defaultDate, - defaultView, + defaultFreeView, + defaultPremiumView, showEventTime, showWeekends, showAllDay, @@ -309,8 +321,10 @@ let CalendarBasicComp = (function () { } catch (error) { initialDate = undefined; } - - + let defaultView = defaultFreeView; + if (licenceKey != "") { + defaultView = defaultPremiumView; + } return ( { @@ -362,7 +378,7 @@ let CalendarBasicComp = (function () { setLeft(left); }} buttonText={buttonText} - schedulerLicenseKey={licenceKey} + schedulerLicenseKey={"CC-Attribution-NonCommercial-NoDerivatives"} views={views} eventClassNames={() => (!showEventTime ? "no-time" : "")} slotLabelFormat={slotLabelFormat} @@ -422,6 +438,7 @@ let CalendarBasicComp = (function () { ); }) .setPropertyViewFn((children) => { + let licence = children.licenceKey.getView(); return ( <>
@@ -435,16 +452,21 @@ let CalendarBasicComp = (function () {
{children.editable.propertyView({ - label: trans("calendar.editable"), - })} + label: trans("calendar.editable"), + })} {children.defaultDate.propertyView({ label: trans("calendar.defaultDate"), tooltip: trans("calendar.defaultDateTooltip"), })} - {children.defaultView.propertyView({ - label: trans("calendar.defaultView"), - tooltip: trans("calendar.defaultViewTooltip"), - })} + {licence == "" + ? children.defaultFreeView.propertyView({ + label: trans("calendar.defaultView"), + tooltip: trans("calendar.defaultViewTooltip"), + }) + : children.defaultPremiumView.propertyView({ + label: trans("calendar.defaultView"), + tooltip: trans("calendar.defaultViewTooltip"), + })} {children.firstDay.propertyView({ label: trans("calendar.startWeek"), })} diff --git a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarConstants.tsx b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarConstants.tsx index bbc02cafd..394c0427a 100644 --- a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarConstants.tsx +++ b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarConstants.tsx @@ -205,9 +205,14 @@ export const Wrapper = styled.div<{ flex-direction: inherit; } .fc-day-today .fc-daygrid-day-number { - background-color: ${(props) => props.$theme?.primary ? props.$theme.primary : props.$style.background}; + background-color: ${(props) => + props.$theme?.primary ? props.$theme.primary : props.$style.background}; color: ${(props) => - contrastText(props.$theme?.primary || "", props.$theme?.textDark || "#000000", props.$theme?.textLight || "#ffffff")}; + contrastText( + props.$theme?.primary || "", + props.$theme?.textDark || "#000000", + props.$theme?.textLight || "#ffffff" + )}; } .fc-daygrid-day-events { padding: 1px 0 5px 0; @@ -330,7 +335,8 @@ export const Wrapper = styled.div<{ height: 20px; padding-left: 15px; font-weight: 500; - background-color: ${(props) => lightenColor(props.$style.background, 0.1)}; + background-color: ${(props) => + lightenColor(props.$style.background, 0.1)}; } } } @@ -368,7 +374,7 @@ export const Wrapper = styled.div<{ } &:hover { .event-remove { - opacity: ${(props) => props.$editable ? 1 : undefined}; + opacity: ${(props) => (props.$editable ? 1 : undefined)}; } } } @@ -398,7 +404,8 @@ export const Wrapper = styled.div<{ // border-radius, bg .fc-theme-standard .fc-list { background-color: ${(props) => props.$style.background}; - border-radius: ${(props) => `0 0 ${props.$style.radius} ${props.$style.radius}`}; + border-radius: ${(props) => + `0 0 ${props.$style.radius} ${props.$style.radius}`}; border-color: ${(props) => props.$style.border}; border-top-color: ${(props) => toHex(props.$style.border) === "#D7D9E0" @@ -406,7 +413,8 @@ export const Wrapper = styled.div<{ : lightenColor(props.$style.border, 0.03)}; } .fc-scrollgrid-liquid { - border-radius: ${(props) => `0 0 ${props.$style.radius} ${props.$style.radius}`}; + border-radius: ${(props) => + `0 0 ${props.$style.radius} ${props.$style.radius}`}; overflow: hidden; border-right-width: 1px; border-bottom-width: 1px; @@ -459,7 +467,8 @@ export const Wrapper = styled.div<{ margin-bottom: 0; border: 1px solid ${(props) => props.$style.border}; border-bottom: none; - border-radius: ${(props) => `${props.$style.radius} ${props.$style.radius} 0 0`}; + border-radius: ${(props) => + `${props.$style.radius} ${props.$style.radius} 0 0`}; background-color: ${(props) => props.$style.background}; } .fc-toolbar-title { @@ -488,7 +497,9 @@ export const Wrapper = styled.div<{ border-color: ${(props) => toHex(props.$style.headerBtnBackground) === "#FFFFFF" ? "#D7D9E0" - : backgroundToBorder(genHoverColor(props.$style.headerBtnBackground))}; + : backgroundToBorder( + genHoverColor(props.$style.headerBtnBackground) + )}; } } &:not(:disabled):focus { @@ -500,7 +511,8 @@ export const Wrapper = styled.div<{ &, &:hover { background-color: ${(props) => props.$style.headerBtnBackground}; - border-color: ${(props) => backgroundToBorder(props.$style.headerBtnBackground)}; + border-color: ${(props) => + backgroundToBorder(props.$style.headerBtnBackground)}; color: ${(props) => toHex(props.$style.btnText) === "#222222" ? "#B8B9BF" @@ -518,7 +530,8 @@ export const Wrapper = styled.div<{ font-size: 14px; margin-left: 8px; background-color: ${(props) => props.$style.headerBtnBackground}; - border-color: ${(props) => backgroundToBorder(props.$style.headerBtnBackground)}; + border-color: ${(props) => + backgroundToBorder(props.$style.headerBtnBackground)}; color: ${(props) => props.$style.btnText}; &.fc-today-button { min-width: 52px; @@ -538,8 +551,8 @@ export const Wrapper = styled.div<{ toHex(props.$style.headerBtnBackground) === "#FFFFFF" ? "#EFEFF1" : isDarkColor(props.$style.headerBtnBackground) - ? props.$style.headerBtnBackground - : darkenColor(props.$style.headerBtnBackground, 0.1)}; + ? props.$style.headerBtnBackground + : darkenColor(props.$style.headerBtnBackground, 0.1)}; border-radius: 4px; margin-left: 16px; .fc-button-primary { @@ -585,10 +598,13 @@ export const Wrapper = styled.div<{ } .fc-day-today.fc-col-header-cell { background-color: ${(props) => - isDarkColor(props.$style.background) ? "#ffffff19" : toHex(props.$theme?.primary!) + "19"}; + isDarkColor(props.$style.background) + ? "#ffffff19" + : toHex(props.$theme?.primary!) + "19"}; a { color: ${(props) => - !isDarkColor(props.$style.background) && darkenColor(props.$theme?.primary!, 0.1)}; + !isDarkColor(props.$style.background) && + darkenColor(props.$theme?.primary!, 0.1)}; } } .fc-col-header-cell-cushion { @@ -649,7 +665,8 @@ export const Event = styled.div<{ box-shadow: ${(props) => !props.isList && "0 0 5px 0 rgba(0, 0, 0, 0.15)"}; border: 1px solid ${(props) => props.$style.border}; display: ${(props) => props.isList && "flex"}; - background-color: ${(props) => !props.isList && lightenColor(props.$style.background, 0.1)}; + background-color: ${(props) => + !props.isList && lightenColor(props.$style.background, 0.1)}; overflow: hidden; font-size: 13px; line-height: 19px; @@ -671,7 +688,9 @@ export const Event = styled.div<{ .event-time { color: ${(props) => !props.isList && - (isDarkColor(props.$style.text) ? lightenColor(props.$style.text, 0.2) : props.$style.text)}; + (isDarkColor(props.$style.text) + ? lightenColor(props.$style.text, 0.2) + : props.$style.text)}; margin-left: 15px; white-space: pre-wrap; margin-top: 2px; @@ -710,14 +729,15 @@ export const Event = styled.div<{ } } &.past { - background-color: ${(props) => isDarkColor(props.$style.background) && props.$style.background}; + background-color: ${(props) => + isDarkColor(props.$style.background) && props.$style.background}; &::before { background-color: ${(props) => toHex(props.$style.text) === "#3C3C3C" ? "#8B8FA3" : isDarkColor(props.$style.text) - ? lightenColor(props.$style.text, 0.3) - : props.$style.text}; + ? lightenColor(props.$style.text, 0.3) + : props.$style.text}; } &::before, .event-title, @@ -758,9 +778,34 @@ export enum ViewType { WEEK = "timeGridWeek", DAY = "timeGridDay", LIST = "listWeek", + TIMEGRID = "timeGridDay", } -export const DefaultViewOptions = [ + +export const DefaultWithPremiumViewOptions = [ + { + label: trans("calendar.month"), + value: "dayGridMonth", + }, + { + label: trans("calendar.week"), + value: "timeGridWeek", + }, + { + label: trans("calendar.timeline"), + value: "resourceTimeline", + }, + { + label: trans("calendar.day"), + value: "timeGridDay", + }, + { + label: trans("calendar.list"), + value: "listWeek", + }, +] as const; + +export const DefaultWithFreeViewOptions = [ { label: trans("calendar.month"), value: "dayGridMonth", @@ -815,7 +860,7 @@ export const defaultData = [ id: "1", title: "Coding", start: dayjs().hour(10).minute(0).second(0).format(DATE_TIME_FORMAT), - end: dayjs().hour(11).minute(30).second(0).format(DATE_TIME_FORMAT), + end: dayjs().hour(12).minute(30).second(0).format(DATE_TIME_FORMAT), color: "#079968", }, { @@ -831,6 +876,7 @@ export const buttonText = { today: trans("calendar.today"), month: trans("calendar.month"), week: trans("calendar.week"), + timeline: trans("calendar.timeline"), day: trans("calendar.day"), list: trans("calendar.list"), }; @@ -843,7 +889,9 @@ export const headerToolbar = { const weekHeadContent = (info: DayHeaderContentArg) => { const text = info.text.split(" "); return { - html: ` + html: ` ${text[0]} ${text[1]} `, 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 048650e24..18c022991 100644 --- a/client/packages/lowcoder-comps/src/i18n/comps/locales/en.ts +++ b/client/packages/lowcoder-comps/src/i18n/comps/locales/en.ts @@ -127,6 +127,7 @@ export const en = { week: "Week", day: "Day", list: "List", + timeline: "TimeLine", //added by fred monday: "Monday", tuesday: "Tuesday", wednesday: "Wednesday", diff --git a/client/yarn.lock b/client/yarn.lock index 385109146..a852f80e2 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2542,6 +2542,17 @@ __metadata: languageName: node linkType: hard +"@fullcalendar/adaptive@npm:^6.1.11": + version: 6.1.11 + resolution: "@fullcalendar/adaptive@npm:6.1.11" + dependencies: + "@fullcalendar/premium-common": ~6.1.11 + peerDependencies: + "@fullcalendar/core": ~6.1.11 + checksum: abfead327433c7142eec5abae9cce7af217f86218c95aa156ab028ae87f90edfc4336090a55764050861aab221217cea5dc7a80658c26f795004eb9d3c290f92 + languageName: node + linkType: hard + "@fullcalendar/core@npm:^6.1.6": version: 6.1.10 resolution: "@fullcalendar/core@npm:6.1.10" @@ -2560,6 +2571,15 @@ __metadata: languageName: node linkType: hard +"@fullcalendar/daygrid@npm:~6.1.11": + version: 6.1.11 + resolution: "@fullcalendar/daygrid@npm:6.1.11" + peerDependencies: + "@fullcalendar/core": ~6.1.11 + checksum: 6eb5606de58b7a8ec30d96618a6d15b2c0d7108c94593ff94e81a8d87ce8efb1f29f3849c6c3f2b8ae56198ffe6235e2ec0e4a1270993c022dc194016e595685 + languageName: node + linkType: hard + "@fullcalendar/interaction@npm:^6.1.6": version: 6.1.10 resolution: "@fullcalendar/interaction@npm:6.1.10" @@ -2588,6 +2608,15 @@ __metadata: languageName: node linkType: hard +"@fullcalendar/premium-common@npm:~6.1.11": + version: 6.1.11 + resolution: "@fullcalendar/premium-common@npm:6.1.11" + peerDependencies: + "@fullcalendar/core": ~6.1.11 + checksum: 54751d6a7245ecec3005450084e4492a2938d90d8538376840572c98ced36101b5a6ea0ce654d3bf98ad173d2e7d45c6fe5d6c530dca785c48e74dcb1eb3556f + languageName: node + linkType: hard + "@fullcalendar/react@npm:^6.1.6": version: 6.1.10 resolution: "@fullcalendar/react@npm:6.1.10" @@ -2599,6 +2628,69 @@ __metadata: languageName: node linkType: hard +"@fullcalendar/resource-daygrid@npm:~6.1.11": + version: 6.1.11 + resolution: "@fullcalendar/resource-daygrid@npm:6.1.11" + dependencies: + "@fullcalendar/daygrid": ~6.1.11 + "@fullcalendar/premium-common": ~6.1.11 + peerDependencies: + "@fullcalendar/core": ~6.1.11 + "@fullcalendar/resource": ~6.1.11 + checksum: afa8a9e240afd9678a5b22c243e64f89568ef3b4d4de62dece6d6d84915774a65f39ff7bc476add50c06a53716fc4011c5a23b19cd47f2723805bcc81f113cfa + languageName: node + linkType: hard + +"@fullcalendar/resource-timegrid@npm:^6.1.11": + version: 6.1.11 + resolution: "@fullcalendar/resource-timegrid@npm:6.1.11" + dependencies: + "@fullcalendar/premium-common": ~6.1.11 + "@fullcalendar/resource-daygrid": ~6.1.11 + "@fullcalendar/timegrid": ~6.1.11 + peerDependencies: + "@fullcalendar/core": ~6.1.11 + "@fullcalendar/resource": ~6.1.11 + checksum: 5e85ef1338cc627598ca644e8f8bcca9f4dab049dd4df1edacf2f9a732663c82ee14fac03552e5b31f9d03804c853c02485d3452e062184e7cd0264ba8161719 + languageName: node + linkType: hard + +"@fullcalendar/resource-timeline@npm:^6.1.11": + version: 6.1.11 + resolution: "@fullcalendar/resource-timeline@npm:6.1.11" + dependencies: + "@fullcalendar/premium-common": ~6.1.11 + "@fullcalendar/scrollgrid": ~6.1.11 + "@fullcalendar/timeline": ~6.1.11 + peerDependencies: + "@fullcalendar/core": ~6.1.11 + "@fullcalendar/resource": ~6.1.11 + checksum: ad9d27a642f097e6f50d1277dd1a09ea8789d7e936ac995e9287e86c8b346dd173c31018d0b29474ebae0fbd5b4a44720fc05db5b671394903767f4095695e2b + languageName: node + linkType: hard + +"@fullcalendar/resource@npm:^6.1.11": + version: 6.1.11 + resolution: "@fullcalendar/resource@npm:6.1.11" + dependencies: + "@fullcalendar/premium-common": ~6.1.11 + peerDependencies: + "@fullcalendar/core": ~6.1.11 + checksum: 6b7266f2e3be6920d3e70fe31aaa42c8eb0f8962d76f79acfeb2e52b50f9f3fac1b98644d5f9b89b5b4109daeb961de6cbd32b58b5b215c426ca0a9534fa6b14 + languageName: node + linkType: hard + +"@fullcalendar/scrollgrid@npm:~6.1.11": + version: 6.1.11 + resolution: "@fullcalendar/scrollgrid@npm:6.1.11" + dependencies: + "@fullcalendar/premium-common": ~6.1.11 + peerDependencies: + "@fullcalendar/core": ~6.1.11 + checksum: f7e54b33245170fd0a696898fdd4076644b3772132ba35996558f7d80836fb1161ccd180a4f064dd8aa5a4f728a1abee063ef909b28ac3a3be46e168f6976ede + languageName: node + linkType: hard + "@fullcalendar/timegrid@npm:^6.1.6": version: 6.1.10 resolution: "@fullcalendar/timegrid@npm:6.1.10" @@ -2610,6 +2702,29 @@ __metadata: languageName: node linkType: hard +"@fullcalendar/timegrid@npm:~6.1.11": + version: 6.1.11 + resolution: "@fullcalendar/timegrid@npm:6.1.11" + dependencies: + "@fullcalendar/daygrid": ~6.1.11 + peerDependencies: + "@fullcalendar/core": ~6.1.11 + checksum: 4a11e6dd908e7d7f660149e6d61eff847efa14d0dcf532f8793de6b035d1a573ef7423fea0df791b6dc5f3d9792df77b72c7e6a1150289d04eca3ff9959a80ec + languageName: node + linkType: hard + +"@fullcalendar/timeline@npm:~6.1.11": + version: 6.1.11 + resolution: "@fullcalendar/timeline@npm:6.1.11" + dependencies: + "@fullcalendar/premium-common": ~6.1.11 + "@fullcalendar/scrollgrid": ~6.1.11 + peerDependencies: + "@fullcalendar/core": ~6.1.11 + checksum: 79341e274a69ae9a63ec67c39b6233ab937daf51177b3689cbe91baf216729805a3fffc12febccece768410b773ae7bb7c51a3eb72b4de64acafedef2b953eff + languageName: node + linkType: hard + "@gilbarbara/deep-equal@npm:^0.1.1": version: 0.1.2 resolution: "@gilbarbara/deep-equal@npm:0.1.2" @@ -11833,12 +11948,16 @@ __metadata: version: 0.0.0-use.local resolution: "lowcoder-comps@workspace:packages/lowcoder-comps" dependencies: + "@fullcalendar/adaptive": ^6.1.11 "@fullcalendar/core": ^6.1.6 "@fullcalendar/daygrid": ^6.1.6 "@fullcalendar/interaction": ^6.1.6 "@fullcalendar/list": ^6.1.9 "@fullcalendar/moment": ^6.1.6 "@fullcalendar/react": ^6.1.6 + "@fullcalendar/resource": ^6.1.11 + "@fullcalendar/resource-timegrid": ^6.1.11 + "@fullcalendar/resource-timeline": ^6.1.11 "@fullcalendar/timegrid": ^6.1.6 "@types/react": ^18.2.45 "@types/react-dom": ^18.2.18 From 4b865841fd9923b1f4f320218319117a8ea856fa Mon Sep 17 00:00:00 2001 From: freddysundowner Date: Mon, 26 Feb 2024 16:03:54 +0300 Subject: [PATCH 05/32] removed hand coded licence key --- .../lowcoder-comps/src/comps/calendarComp/calendarComp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx index 195e5c1fb..c65de7873 100644 --- a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx @@ -378,7 +378,7 @@ let CalendarBasicComp = (function () { setLeft(left); }} buttonText={buttonText} - schedulerLicenseKey={"CC-Attribution-NonCommercial-NoDerivatives"} + schedulerLicenseKey={licenceKey} views={views} eventClassNames={() => (!showEventTime ? "no-time" : "")} slotLabelFormat={slotLabelFormat} From 50f87fc936268ae5c860fbae0a341f4a3c604750 Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Mon, 26 Feb 2024 20:52:51 +0500 Subject: [PATCH 06/32] Mention component height issue fix, checkbox component hover background color property added along with translations of hover background text --- .../comps/selectInputComp/checkboxComp.tsx | 16 +++++++++- .../comps/comps/textInputComp/mentionComp.tsx | 9 ++++-- .../comps/controls/styleControlConstants.tsx | 30 +++++-------------- .../packages/lowcoder/src/i18n/locales/de.ts | 1 + .../packages/lowcoder/src/i18n/locales/en.ts | 1 + .../packages/lowcoder/src/i18n/locales/zh.ts | 1 + 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx index 5a4a22e17..e6442b72b 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx @@ -51,13 +51,25 @@ export const getStyle = (style: CheckboxStyleType) => { border-radius: ${style.radius}; } } - + .ant-checkbox-inner { border-radius: ${style.radius}; background-color: ${style.uncheckedBackground}; border-color: ${style.uncheckedBorder}; border-width:${!!style.borderWidth ? style.borderWidth : '2px'}; } + + &:hover .ant-checkbox-inner, + .ant-checkbox:hover .ant-checkbox-inner, + .ant-checkbox-input + ant-checkbox-inner { + background-color:${style.hoverBackground ? style.hoverBackground :'#fff'}; + } + + &:hover .ant-checkbox-checked .ant-checkbox-inner, + .ant-checkbox:hover .ant-checkbox-inner, + .ant-checkbox-input + ant-checkbox-inner { + background-color:${style.hoverBackground ? style.hoverBackground:'#ffff'}; + } &:hover .ant-checkbox-inner, .ant-checkbox:hover .ant-checkbox-inner, @@ -67,6 +79,8 @@ export const getStyle = (style: CheckboxStyleType) => { } } + + .ant-checkbox-group-item { font-family:${style.fontFamily}; font-size:${style.textSize}; diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx index c760e054b..6bb38eb66 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx @@ -58,7 +58,12 @@ import { EditorContext } from "comps/editorState"; const Wrapper = styled.div<{ $style: InputLikeStyleType; }>` - height: 100%; + box-sizing:border-box; + .rc-textarea { + background-color:${(props) => props.$style.background}; + padding:${(props) => props.$style.padding}; + margin: 0px 3px 0px 3px !important; + } .ant-input-clear-icon { opacity: 0.45; @@ -196,7 +201,7 @@ let MentionTmpComp = (function () { height: "100%", maxHeight: "100%", resize: "none", - padding: props.style.padding, + // padding: props.style.padding, fontStyle: props.style.fontStyle, fontFamily: props.style.fontFamily, borderWidth: props.style.borderWidth, diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 30513a9af..df95e2826 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -350,6 +350,12 @@ const TEXT_WEIGHT = { textWeight: "textWeight", } as const; +const HOVER_BACKGROUND_COLOR = { + name: "hoverBackground", + label: trans("style.hoverBackground"), + hoverBackground: "hoverBackground" +} + const FONT_FAMILY = { name: "fontFamily", label: trans("style.fontFamily"), @@ -476,7 +482,6 @@ function replaceAndMergeMultipleStyles(originalArray: any[], styleToReplace: str } export const ButtonStyle = [ - // ...getBgBorderRadiusByBg("primary"), getBackground('primary'), ...STYLING_FIELDS_SEQUENCE ] as const; @@ -735,23 +740,12 @@ export const SwitchStyle = [ ] as const; export const SelectStyle = [ - // LABEL, ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'border', [...getStaticBgBorderRadiusByBg(SURFACE_COLOR, "pc")]), - - // ...getStaticBgBorderRadiusByBg(SURFACE_COLOR, "pc"), - // TEXT, - // MARGIN, - // PADDING, ...ACCENT_VALIDATE, ] as const; const multiSelectCommon = [ ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'border', [...getStaticBgBorderRadiusByBg(SURFACE_COLOR, "pc")]), - // LABEL, - // ...getStaticBgBorderRadiusByBg(SURFACE_COLOR, "pc"), - // TEXT, - // MARGIN, - // PADDING, { name: "tags", label: trans("style.tags"), @@ -844,7 +838,6 @@ function checkAndUncheck() { } export const CheckboxStyle = [ - // LABEL, ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'text', [LABEL, STATIC_TEXT, VALIDATE]).filter((style) => style.name !== 'border'), ...checkAndUncheck(), { @@ -854,15 +847,10 @@ export const CheckboxStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, }, - // RADIUS, - // STATIC_TEXT, - // VALIDATE, - // MARGIN, - // PADDING, + HOVER_BACKGROUND_COLOR ] as const; export const RadioStyle = [ - // LABEL, ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'text', [LABEL, STATIC_TEXT, VALIDATE]).filter((style) => style.name !== 'border' && style.name !== 'radius'), ...checkAndUncheck(), { @@ -872,10 +860,6 @@ export const RadioStyle = [ depType: DEP_TYPE.SELF, transformer: toSelf, }, - // STATIC_TEXT, - // VALIDATE, - // MARGIN, - // PADDING, ] as const; export const SegmentStyle = [ diff --git a/client/packages/lowcoder/src/i18n/locales/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index 298e80ef4..b11797bc2 100644 --- a/client/packages/lowcoder/src/i18n/locales/de.ts +++ b/client/packages/lowcoder/src/i18n/locales/de.ts @@ -324,6 +324,7 @@ export const de = { "tableCellText": "Zelle Text", "selectedRowBackground": "Ausgewählter Zeilenhintergrund", "hoverRowBackground": "Hover Row Hintergrund", + "hoverBackground":"Hover-Hintergrund", "alternateRowBackground": "Alternativer Reihenhintergrund", "tableHeaderBackground": "Kopfzeile Hintergrund", "tableHeaderText": "Überschrift Text", diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index fc05496d4..cbcb2f19d 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -339,6 +339,7 @@ export const en = { "tableCellText": "Cell Text", "selectedRowBackground": "Selected Row Background", "hoverRowBackground": "Hover Row Background", + "hoverBackground":"Hover Background", "alternateRowBackground": "Alternate Row Background", "tableHeaderBackground": "Header Background", "tableHeaderText": "Header Text", diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index a91e25bff..ebfcc5ba6 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -330,6 +330,7 @@ style: { tableCellText: "单元格文本", selectedRowBackground: "选中行背景", hoverRowBackground: "悬停行背景", + hoverBackground:"悬停背景", alternateRowBackground: "交替行背景", tableHeaderBackground: "表头背景", tableHeaderText: "表头文本", From 658e29938609a2800e98a90e36524ce43d0d4fc9 Mon Sep 17 00:00:00 2001 From: Abdul Qadir Date: Tue, 27 Feb 2024 20:44:59 +0500 Subject: [PATCH 07/32] Add logs for debugging --- .../java/org/lowcoder/infra/serverlog/ServerLogService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java index e975ba407..b45708a20 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java @@ -51,6 +51,8 @@ public Mono getApiUsageCount(String orgId, Boolean lastMonthOnly) { if(lastMonthOnly != null && lastMonthOnly) { Long startMonthEpoch = LocalDateTime.now().minusMonths(1).with(TemporalAdjusters.firstDayOfMonth()).toEpochSecond(ZoneOffset.UTC)*1000; Long endMonthEpoch = LocalDateTime.now().minusMonths(1).with(TemporalAdjusters.lastDayOfMonth()).toEpochSecond(ZoneOffset.UTC)*1000; + System.out.println("startMonthEpoch is: " + startMonthEpoch); + System.out.println("endMonthEpoch is: " + endMonthEpoch); return serverLogRepository.countByOrgIdAndCreateTimeBetween(orgId, startMonthEpoch, endMonthEpoch); } return serverLogRepository.countByOrgId(orgId); From 4890116257330c0fa14fbb5a5815a1619826db98 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Tue, 27 Feb 2024 20:56:13 +0500 Subject: [PATCH 08/32] fix for comps hide on adding to modal/drawer --- .../packages/lowcoder/src/layout/gridLayout.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/client/packages/lowcoder/src/layout/gridLayout.tsx b/client/packages/lowcoder/src/layout/gridLayout.tsx index 6b697c0c0..83facb75e 100644 --- a/client/packages/lowcoder/src/layout/gridLayout.tsx +++ b/client/packages/lowcoder/src/layout/gridLayout.tsx @@ -172,12 +172,25 @@ class GridLayout extends React.Component { } componentDidUpdate(prevProps: GridLayoutProps, prevState: GridLayoutState) { - const uiLayout = this.getUILayout(); if (!draggingUtils.isDragging()) { // log.debug("render. clear ops. layout: ", uiLayout); // only change in changeHs, don't change state if (_.size(this.state.ops) > 0) { - this.setState({ layout: uiLayout, changedHs: undefined, ops: undefined }); + // temporary fix for components becomes invisible in drawer/modal + // TODO: find a way to call DELETE_ITEM operation after layouts are updated in state + const ops = [...this.state.ops as any[]]; + const [firstOp] = ops; + const { droppingItem } = this.props; + if( + ops.length === 1 + && firstOp.type === 'DELETE_ITEM' + && firstOp.key === droppingItem?.i + ) { + this.setState({ changedHs: undefined, ops: undefined }); + } else { + const uiLayout = this.getUILayout(); + this.setState({ layout: uiLayout, changedHs: undefined, ops: undefined }) + } } } if (!draggingUtils.isDragging() && _.isNil(this.state.ops)) { From 6b8a4977b6fefa7418c56451e86ddac7a3a61774 Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Tue, 27 Feb 2024 21:37:28 +0500 Subject: [PATCH 09/32] Fixes and introduction of text-decoration and text transform property with css control --- .../comps/buttonComp/buttonCompConstants.tsx | 2 ++ .../comps/comps/buttonComp/dropdownComp.tsx | 11 ++++++++ .../src/comps/comps/buttonComp/linkComp.tsx | 2 ++ .../lowcoder/src/comps/comps/dividerComp.tsx | 2 ++ .../src/comps/comps/navComp/navComp.tsx | 6 +++++ .../comps/selectInputComp/checkboxComp.tsx | 2 ++ .../comps/comps/selectInputComp/radioComp.tsx | 8 ++++++ .../lowcoder/src/comps/comps/textComp.tsx | 3 ++- .../comps/comps/textInputComp/mentionComp.tsx | 2 ++ .../textInputComp/textInputConstants.tsx | 2 ++ .../src/comps/controls/styleControl.tsx | 2 ++ .../comps/controls/styleControlConstants.tsx | 27 +++++++++++++++++-- .../packages/lowcoder/src/i18n/locales/de.ts | 2 ++ .../packages/lowcoder/src/i18n/locales/en.ts | 2 ++ .../packages/lowcoder/src/i18n/locales/zh.ts | 2 ++ 15 files changed, 72 insertions(+), 3 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/buttonCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/buttonCompConstants.tsx index e770014f7..8ebcd250c 100644 --- a/client/packages/lowcoder/src/comps/comps/buttonComp/buttonCompConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/buttonComp/buttonCompConstants.tsx @@ -24,6 +24,8 @@ export function getButtonStyle(buttonStyle: ButtonStyleType) { font-weight: ${buttonStyle.textWeight}; font-family: ${buttonStyle.fontFamily}; font-style: ${buttonStyle.fontStyle}; + text-transform:${buttonStyle.textTransform}; + text-decoration:${buttonStyle.textDecoration}; background-color: ${buttonStyle.background}; border-radius: ${buttonStyle.radius}; margin: ${buttonStyle.margin}; diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/dropdownComp.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/dropdownComp.tsx index 7c2278d7b..93cac0fae 100644 --- a/client/packages/lowcoder/src/comps/comps/buttonComp/dropdownComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/buttonComp/dropdownComp.tsx @@ -25,8 +25,10 @@ import { const StyledDropdownButton = styled(DropdownButton)` width: 100%; + .ant-btn-group { width: 100%; + } `; @@ -34,6 +36,11 @@ const LeftButtonWrapper = styled.div<{ $buttonStyle: ButtonStyleType }>` width: calc(100%); ${(props) => `margin: ${props.$buttonStyle.margin};`} margin-right: 0; + .ant-btn span { + ${(props) => `text-decoration: ${props.$buttonStyle.textDecoration};`} + ${(props) => `font-family: ${props.$buttonStyle.fontFamily};`} + } + .ant-btn { ${(props) => getButtonStyle(props.$buttonStyle)} margin: 0 !important; @@ -41,14 +48,18 @@ const LeftButtonWrapper = styled.div<{ $buttonStyle: ButtonStyleType }>` &.ant-btn-default { margin: 0 !important; ${(props) => `border-radius: ${props.$buttonStyle.radius} 0 0 ${props.$buttonStyle.radius};`} + ${(props) => `text-transform: ${props.$buttonStyle.textTransform};`} + ${(props) => `font-weight: ${props.$buttonStyle.textWeight};`} } ${(props) => `background-color: ${props.$buttonStyle.background};`} ${(props) => `color: ${props.$buttonStyle.text};`} ${(props) => `padding: ${props.$buttonStyle.padding};`} ${(props) => `font-size: ${props.$buttonStyle.textSize};`} ${(props) => `font-style: ${props.$buttonStyle.fontStyle};`} + width: 100%; } + `; const RightButtonWrapper = styled.div<{ $buttonStyle: ButtonStyleType }>` diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/linkComp.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/linkComp.tsx index cbd5b26b2..274a29e13 100644 --- a/client/packages/lowcoder/src/comps/comps/buttonComp/linkComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/buttonComp/linkComp.tsx @@ -34,6 +34,8 @@ const Link = styled(Button) <{ $style: LinkStyleType }>` font-weight:${props.$style.textWeight}; border: ${props.$style.borderWidth} solid ${props.$style.border}; border-radius:${props.$style.radius ? props.$style.radius:'0px'}; + text-transform:${props.$style.textTransform ? props.$style.textTransform:''}; + text-decoration:${props.$style.textDecoration ? props.$style.textDecoration:''} !important; background-color: ${props.$style.background}; &:hover { color: ${props.$style.hoverText} !important; diff --git a/client/packages/lowcoder/src/comps/comps/dividerComp.tsx b/client/packages/lowcoder/src/comps/comps/dividerComp.tsx index 032807de2..906db77c3 100644 --- a/client/packages/lowcoder/src/comps/comps/dividerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dividerComp.tsx @@ -30,6 +30,8 @@ const StyledDivider = styled(Divider) ` font-size: ${(props) => props.$style.textSize}; font-weight: ${(props) => props.$style.textWeight}; font-family: ${(props) => props.$style.fontFamily}; + text-transform:${(props)=>props.$style.textTransform}; + text-decoration:${(props)=>props.$style.textDecoration}; font-style:${(props) => props.$style.fontStyle} } min-width: 0; diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 44b782b47..25882da30 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -52,6 +52,8 @@ const Item = styled.div<{ $textSize: string; $margin: string; $padding: string; + $textTransform:string; + $textDecoration:string; }>` height: 30px; line-height: 30px; @@ -61,6 +63,8 @@ const Item = styled.div<{ font-family:${(props) => (props.$fontFamily ? props.$fontFamily : 'sans-serif')}; font-style:${(props) => (props.$fontStyle ? props.$fontStyle : 'normal')}; font-size:${(props) => (props.$textSize ? props.$textSize : '14px')}; + text-transform:${(props) => (props.$textTransform ? props.$textTransform : '')}; + text-decoration:${(props) => (props.$textDecoration ? props.$textDecoration : '')}; margin:${(props) => props.$margin ? props.$margin : '0px'}; &:hover { @@ -161,6 +165,8 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => { $textWeight={props.style.textWeight} $textSize={props.style.textSize} $padding={props.style.padding} + $textTransform={props.style.textTransform} + $textDecoration={props.style.textDecoration} $margin={props.style.margin} onClick={() => onEvent("click")} > diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx index e6442b72b..56182c40c 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx @@ -86,6 +86,8 @@ export const getStyle = (style: CheckboxStyleType) => { font-size:${style.textSize}; font-weight:${style.textWeight}; font-style:${style.fontStyle}; + text-transform:${style.textTransform}; + text-decoration:${style.textDecoration}; } .ant-checkbox-wrapper { padding: ${style.padding}; diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx index af98eee25..a059f8c03 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx @@ -25,6 +25,8 @@ const getStyle = (style: RadioStyleType) => { font-size:${style.textSize}; font-weight:${style.textWeight}; font-style:${style.fontStyle}; + text-transform:${style.textTransform}; + text-decoration:${style.textDecoration}; } .ant-radio-checked { @@ -47,6 +49,12 @@ const getStyle = (style: RadioStyleType) => { } } + &:hover .ant-radio-inner, + .ant-radio:hover .ant-radio-inner, + .ant-radio-input + ant-radio-inner { + background-color:${style.hoverBackground ? style.hoverBackground:'#ffff'}; + } + &:hover .ant-radio-inner, .ant-radio:hover .ant-radio-inner, .ant-radio-input:focus + .ant-radio-inner { diff --git a/client/packages/lowcoder/src/comps/comps/textComp.tsx b/client/packages/lowcoder/src/comps/comps/textComp.tsx index 4359166dd..0798f07f2 100644 --- a/client/packages/lowcoder/src/comps/comps/textComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textComp.tsx @@ -30,6 +30,8 @@ const getStyle = (style: TextStyleType) => { font-weight: ${style.textWeight} !important; font-family: ${style.fontFamily} !important; font-style:${style.fontStyle} !important; + text-transform:${style.textTransform} !important; + text-decoration:${style.textDecoration} !important; background-color: ${style.background}; .markdown-body a { color: ${style.links}; @@ -146,7 +148,6 @@ let TextTmpComp = (function () { .setPropertyViewFn((children) => { return ( <> -
{children.type.propertyView({ label: trans("value"), diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx index 6bb38eb66..51815260f 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx @@ -62,6 +62,8 @@ const Wrapper = styled.div<{ .rc-textarea { background-color:${(props) => props.$style.background}; padding:${(props) => props.$style.padding}; + text-transform:${(props)=>props.$style.textTransform}; + text-decoration:${(props)=>props.$style.textDecoration}; margin: 0px 3px 0px 3px !important; } diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 5a2438245..772cbdc51 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -230,6 +230,8 @@ export function getStyle(style: InputLikeStyleType) { font-weight: ${style.textWeight}; font-family: ${style.fontFamily}; font-style:${style.fontStyle}; + text-transform:${style.textTransform}; + text-decoration:${style.textDecoration}; background-color: ${style.background}; border-color: ${style.border}; diff --git a/client/packages/lowcoder/src/comps/controls/styleControl.tsx b/client/packages/lowcoder/src/comps/controls/styleControl.tsx index 7295a9783..109801a97 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControl.tsx @@ -547,6 +547,8 @@ export function styleControl(colorConfig name === "cardRadius" || name === "textSize" || name === "textWeight" || + name === "textTransform" || + name === "textDecoration" || name === "fontFamily" || name === "fontStyle" || name === "backgroundImage" || diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index df95e2826..8a539b33d 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -74,13 +74,21 @@ export type PaddingConfig = CommonColorConfig & { readonly padding: string; }; +export type TextTransformConfig = CommonColorConfig & { + readonly textTransform: string; +} + +export type TextDecorationConfig = CommonColorConfig & { + readonly textDecoration: string; +} + export type DepColorConfig = CommonColorConfig & { readonly depName?: string; readonly depTheme?: keyof ThemeDetail; readonly depType?: DEP_TYPE; transformer: (color: string, ...rest: string[]) => string; }; -export type SingleColorConfig = SimpleColorConfig | DepColorConfig | RadiusConfig | BorderWidthConfig | BackgroundImageConfig | BackgroundImageRepeatConfig | BackgroundImageSizeConfig | BackgroundImagePositionConfig | BackgroundImageOriginConfig | TextSizeConfig | TextWeightConfig | FontFamilyConfig | FontStyleConfig | MarginConfig | PaddingConfig | ContainerHeaderPaddigConfig | ContainerFooterPaddigConfig | ContainerBodyPaddigConfig | HeaderBackgroundImageConfig | HeaderBackgroundImageRepeatConfig | HeaderBackgroundImageSizeConfig | HeaderBackgroundImagePositionConfig | HeaderBackgroundImageOriginConfig | FooterBackgroundImageConfig | FooterBackgroundImageRepeatConfig | FooterBackgroundImageSizeConfig | FooterBackgroundImagePositionConfig | FooterBackgroundImageOriginConfig; +export type SingleColorConfig = SimpleColorConfig | DepColorConfig | RadiusConfig | BorderWidthConfig | BackgroundImageConfig | BackgroundImageRepeatConfig | BackgroundImageSizeConfig | BackgroundImagePositionConfig | BackgroundImageOriginConfig | TextSizeConfig | TextWeightConfig | TextTransformConfig | TextDecorationConfig | FontFamilyConfig | FontStyleConfig | MarginConfig | PaddingConfig | ContainerHeaderPaddigConfig | ContainerFooterPaddigConfig | ContainerBodyPaddigConfig | HeaderBackgroundImageConfig | HeaderBackgroundImageRepeatConfig | HeaderBackgroundImageSizeConfig | HeaderBackgroundImagePositionConfig | HeaderBackgroundImageOriginConfig | FooterBackgroundImageConfig | FooterBackgroundImageRepeatConfig | FooterBackgroundImageSizeConfig | FooterBackgroundImagePositionConfig | FooterBackgroundImageOriginConfig; export const defaultTheme: ThemeDetail = { primary: "#3377FF", @@ -387,6 +395,18 @@ const CONTAINERBODYPADDING = { containerbodypadding: "padding", } as const; +const TEXT_TRANSFORM = { + name: "textTransform", + label: trans("style.textTransform"), + textTransform: "textTransform" +} + +const TEXT_DECORATION = { + name: "textDecoration", + label: trans("style.textDecoration"), + textDecoration: "textDecoration" +} + const getStaticBorder = (color: string = SECOND_SURFACE_COLOR) => ({ name: "border", @@ -405,6 +425,8 @@ const HEADER_BACKGROUND = { const BG_STATIC_BORDER_RADIUS = [getBackground(), getStaticBorder(), RADIUS] as const; const STYLING_FIELDS_SEQUENCE = [ TEXT, + TEXT_TRANSFORM, + TEXT_DECORATION, TEXT_SIZE, TEXT_WEIGHT, FONT_FAMILY, @@ -860,6 +882,7 @@ export const RadioStyle = [ depType: DEP_TYPE.SELF, transformer: toSelf, }, + HOVER_BACKGROUND_COLOR ] as const; export const SegmentStyle = [ @@ -1079,7 +1102,7 @@ export const ProgressStyle = [ depTheme: "canvas", depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, - }]).filter((style) => ['border', 'borderWidth'].includes(style.name) === false), + }]).filter((style) => ['border', 'borderWidth','textTransform','textDecoration'].includes(style.name) === false), TRACK, FILL, SUCCESS, diff --git a/client/packages/lowcoder/src/i18n/locales/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index b11797bc2..404520788 100644 --- a/client/packages/lowcoder/src/i18n/locales/de.ts +++ b/client/packages/lowcoder/src/i18n/locales/de.ts @@ -325,6 +325,8 @@ export const de = { "selectedRowBackground": "Ausgewählter Zeilenhintergrund", "hoverRowBackground": "Hover Row Hintergrund", "hoverBackground":"Hover-Hintergrund", + "textTransform":"Texttransformation", + "textDecoration":"Textdekoration", "alternateRowBackground": "Alternativer Reihenhintergrund", "tableHeaderBackground": "Kopfzeile Hintergrund", "tableHeaderText": "Überschrift Text", diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index cbcb2f19d..a01b2c5e9 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -340,6 +340,8 @@ export const en = { "selectedRowBackground": "Selected Row Background", "hoverRowBackground": "Hover Row Background", "hoverBackground":"Hover Background", + "textTransform":"Text Transform", + "textDecoration":"Text Decoration", "alternateRowBackground": "Alternate Row Background", "tableHeaderBackground": "Header Background", "tableHeaderText": "Header Text", diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index ebfcc5ba6..ca3e99f49 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -331,6 +331,8 @@ style: { selectedRowBackground: "选中行背景", hoverRowBackground: "悬停行背景", hoverBackground:"悬停背景", + textTransform:"文本变换", + textDecoration:"文字装饰", alternateRowBackground: "交替行背景", tableHeaderBackground: "表头背景", tableHeaderText: "表头文本", From f50180215956ba5325613a1b1f5171057ee1e86a Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Tue, 27 Feb 2024 22:10:46 +0500 Subject: [PATCH 10/32] CSS styles and propeties added to tabbed container while ensuring the styling rules of container component --- .../comps/comps/selectInputComp/selectCompConstants.tsx | 3 ++- .../src/comps/comps/tabs/tabbedContainerComp.tsx | 8 ++++++++ .../src/comps/controls/styleControlConstants.tsx | 9 +++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx index c4b0d43ae..e277751b4 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx @@ -69,6 +69,8 @@ export const getStyle = ( } .ant-select-selection-search-input { font-family:${(style as SelectStyleType).fontFamily} !important; + text-transform:${(style as SelectStyleType).textTransform} !important; + text-decoration:${(style as SelectStyleType).textDecoration} !important; font-size:${(style as SelectStyleType).textSize} !important; font-weight:${(style as SelectStyleType).textWeight}; color:${(style as SelectStyleType).text} !important; @@ -256,7 +258,6 @@ export const SelectUIView = ( label={option.label} disabled={option.disabled} key={option.value} - style={{fontFamily:"Montserrat"}} > {props.options.findIndex((option) => hasIcon(option.prefixIcon)) > -1 && diff --git a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx index 06a5ad955..1bdc2ea4e 100644 --- a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx @@ -111,6 +111,14 @@ const getStyle = ( } } + .ant-tabs-tab-btn { + font-family:${style.fontFamily}; + font-weight:${style.textWeight}; + text-transform:${style.textTransform}; + text-decoration:${style.textDecoration}; + font-style:${style.fontStyle}; + } + .ant-tabs-ink-bar { background-color: ${style.accent}; } diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 8a539b33d..1b994c062 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -554,6 +554,7 @@ export const MarginStyle = [ export const ContainerStyle = [ // ...BG_STATIC_BORDER_RADIUS, getStaticBorder(), + // ...STYLING_FIELDS_SEQUENCE.filter((style) => style.name !== 'border'), RADIUS, BORDER_WIDTH, MARGIN, @@ -798,13 +799,14 @@ export const MultiSelectStyle = [ ] as const; export const TabContainerStyle = [ - { + // Keep background related properties of container as STYLING_FIELDS_SEQUENCE has rest of the properties + ...replaceAndMergeMultipleStyles([...ContainerStyle.filter((style)=> ['border','radius','borderWidth','margin','padding'].includes(style.name) === false),...STYLING_FIELDS_SEQUENCE], 'text', [{ name: "tabText", label: trans("style.tabText"), depName: "headerBackground", depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, - }, + },]), { name: "accent", label: trans("style.tabAccent"), @@ -812,7 +814,6 @@ export const TabContainerStyle = [ depType: DEP_TYPE.SELF, transformer: toSelf, }, - ...ContainerStyle, ] as const; export const ModalStyle = [ @@ -1102,7 +1103,7 @@ export const ProgressStyle = [ depTheme: "canvas", depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, - }]).filter((style) => ['border', 'borderWidth','textTransform','textDecoration'].includes(style.name) === false), + }]).filter((style) => ['border', 'borderWidth', 'textTransform', 'textDecoration'].includes(style.name) === false), TRACK, FILL, SUCCESS, From 2c800456234ecd5375df427eb1f673be5d90d62d Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Tue, 27 Feb 2024 22:20:41 +0500 Subject: [PATCH 11/32] CSS properties and their control added for segment control --- .../src/comps/comps/selectInputComp/segmentedControl.tsx | 8 ++++++++ .../lowcoder/src/comps/controls/styleControlConstants.tsx | 4 +--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx index bf105982a..c3ac81937 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx @@ -52,6 +52,14 @@ const getStyle = (style: SegmentStyleType) => { .ant-segmented-item-selected { border-radius: ${style.radius}; } + &.ant-segmented, .ant-segmented-item-label { + font-family:${style.fontFamily}; + font-style:${style.fontStyle}; + font-size:${style.textSize}; + font-weight:${style.textWeight}; + text-transform:${style.textTransform}; + text-decoration:${style.textDecoration}; + } `; }; diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 1b994c062..e36cd9d9b 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -888,6 +888,7 @@ export const RadioStyle = [ export const SegmentStyle = [ LABEL, + ...STYLING_FIELDS_SEQUENCE.filter((style)=> ['border','borderWidth'].includes(style.name) === false), { name: "indicatorBackground", label: trans("style.indicatorBackground"), @@ -906,10 +907,7 @@ export const SegmentStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, }, - RADIUS, VALIDATE, - MARGIN, - PADDING, ] as const; const LinkTextStyle = [ From 8e9ab45f420dab208fa0f3117516358ce9708583 Mon Sep 17 00:00:00 2001 From: Andy C <46699959+sudoischenny@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:55:38 -0500 Subject: [PATCH 12/32] Toasts Notification & Loading Mesages Added a built in function called toast for longer notifications. Also added message.loading() Please see docs\build-apps\write-javascript\built-in-javascript-functions.md --- .../lowcoder/src/comps/hooks/hookComp.tsx | 2 + .../src/comps/hooks/hookCompTypes.tsx | 2 + .../lowcoder/src/comps/hooks/hookListComp.tsx | 1 + .../lowcoder/src/comps/hooks/messageComp.ts | 8 +- .../lowcoder/src/comps/hooks/toastComp.ts | 102 ++++++++++++++++++ .../packages/lowcoder/src/i18n/locales/de.ts | 8 ++ .../packages/lowcoder/src/i18n/locales/en.ts | 8 ++ .../packages/lowcoder/src/i18n/locales/zh.ts | 8 ++ docs/.gitbook/assets/builtin-js-toasts.png | Bin 0 -> 31319 bytes .../built-in-javascript-functions.md | 39 +++++-- 10 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/hooks/toastComp.ts create mode 100644 docs/.gitbook/assets/builtin-js-toasts.png diff --git a/client/packages/lowcoder/src/comps/hooks/hookComp.tsx b/client/packages/lowcoder/src/comps/hooks/hookComp.tsx index dd2a6b1dc..8002436e1 100644 --- a/client/packages/lowcoder/src/comps/hooks/hookComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/hookComp.tsx @@ -29,6 +29,7 @@ import { useInterval, useTitle, useWindowSize } from "react-use"; import { useCurrentUser } from "util/currentUser"; import { LocalStorageComp } from "./localStorageComp"; import { MessageComp } from "./messageComp"; +import { ToastComp } from "./toastComp"; import { ThemeComp } from "./themeComp"; import UrlParamsHookComp from "./UrlParamsHookComp"; import { UtilsComp } from "./utilsComp"; @@ -94,6 +95,7 @@ const HookMap: HookCompMapRawType = { momentJsLib: DayJsLib, // old components use this hook utils: UtilsComp, message: MessageComp, + toast: ToastComp, localStorage: LocalStorageComp, modal: ModalComp, meeting: VideoMeetingControllerComp, diff --git a/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx b/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx index a985a8e72..617f2f6c1 100644 --- a/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx +++ b/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx @@ -12,6 +12,7 @@ const AllHookComp = [ "momentJsLib", "utils", "message", + "toast", "localStorage", "currentUser", "screenInfo", @@ -58,6 +59,7 @@ const HookCompConfig: Record< }, utils: { category: "hide" }, message: { category: "hide" }, + toast: { category: "hide" }, }; // Get hook component category diff --git a/client/packages/lowcoder/src/comps/hooks/hookListComp.tsx b/client/packages/lowcoder/src/comps/hooks/hookListComp.tsx index 3f28e2d1a..7b267ec19 100644 --- a/client/packages/lowcoder/src/comps/hooks/hookListComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/hookListComp.tsx @@ -19,6 +19,7 @@ const defaultHookListValue = [ { compType: "lodashJsLib", name: "_" }, { compType: "utils", name: "utils" }, { compType: "message", name: "message" }, + { compType: "toast", name: "toast" }, { compType: "localStorage", name: "localStorage" }, { compType: "currentUser", name: "currentUser" }, { compType: "screenInfo", name: "screenInfo" }, diff --git a/client/packages/lowcoder/src/comps/hooks/messageComp.ts b/client/packages/lowcoder/src/comps/hooks/messageComp.ts index e0e6451cb..028c37691 100644 --- a/client/packages/lowcoder/src/comps/hooks/messageComp.ts +++ b/client/packages/lowcoder/src/comps/hooks/messageComp.ts @@ -11,7 +11,7 @@ const params: ParamsConfig = [ { name: "options", type: "JSON" }, ]; -const showMessage = (params: EvalParamType[], level: "info" | "success" | "warning" | "error") => { +const showMessage = (params: EvalParamType[], level: "info" | "success" | "loading" | "warning" | "error") => { const text = params?.[0]; const options = params?.[1] as JSONObject; const duration = options?.["duration"] ?? 3; @@ -35,6 +35,12 @@ MessageComp = withMethodExposing(MessageComp, [ showMessage(params, "success"); }, }, + { + method: { name: "loading", description: trans("messageComp.loading"), params: params }, + execute: (comp, params) => { + showMessage(params, "loading"); + }, + }, { method: { name: "warn", description: trans("messageComp.warn"), params: params }, execute: (comp, params) => { diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp.ts b/client/packages/lowcoder/src/comps/hooks/toastComp.ts new file mode 100644 index 000000000..604c127ed --- /dev/null +++ b/client/packages/lowcoder/src/comps/hooks/toastComp.ts @@ -0,0 +1,102 @@ +import { withMethodExposing } from "../generators/withMethodExposing"; +import { simpleMultiComp } from "../generators"; +import { withExposingConfigs } from "../generators/withExposing"; +import { EvalParamType, ParamsConfig } from "../controls/actionSelector/executeCompTypes"; +import { JSONObject } from "../../util/jsonTypes"; +import { trans } from "i18n"; +import { notificationInstance } from "lowcoder-design"; +import type { ArgsProps, NotificationPlacement } from 'antd/es/notification/interface'; + +const params: ParamsConfig = [ + { name: "text", type: "string" }, + { name: "options", type: "JSON" }, +]; + +const showNotification = ( + params: EvalParamType[], + level: "open" | "info" | "success" | "warning" | "error" +) => { + const text = params?.[0] as string; + const options = params?.[1] as JSONObject; + + const { message , duration, id, placement } = options; + + // Convert duration to a number or null, if it's not a valid number, default to null + const durationNumberOrNull: number | null = typeof duration === 'number' ? duration : null; + + const notificationArgs: ArgsProps = { + message: text, + description: message as React.ReactNode, + duration: durationNumberOrNull ?? 3, + key: id as React.Key, // Ensure id is a valid React.Key + placement: placement as NotificationPlacement ?? "bottomRight", // Ensure placement is a valid NotificationPlacement or undefined + }; + + // Use notificationArgs to trigger the notification + + text && notificationInstance[level](notificationArgs); +}; + +//what we would like to expose: title, text, duration, id, btn-obj, onClose, placement + +const ToastCompBase = simpleMultiComp({}); + +export let ToastComp = withExposingConfigs(ToastCompBase, []); + +/* +export declare const NotificationPlacements: readonly ["top", "topLeft", "topRight", "bottom", "bottomLeft", "bottomRight"]; +export type NotificationPlacement = (typeof NotificationPlacements)[number]; +export type IconType = 'success' | 'info' | 'error' | 'warning'; +export interface ArgsProps { + message: React.ReactNode; + description?: React.ReactNode; + btn?: React.ReactNode; + key?: React.Key; + onClose?: () => void; + duration?: number | null; + icon?: React.ReactNode; + placement?: NotificationPlacement; + style?: React.CSSProperties; + className?: string; + readonly type?: IconType; + onClick?: () => void; + closeIcon?: React.ReactNode; + props?: DivProps; + role?: 'alert' | 'status'; +} +*/ + +ToastComp = withMethodExposing(ToastComp, [ + { + method: { name: "open", description: trans("toastComp.info"), params: params }, + execute: (comp, params) => { + showNotification(params, "open"); + }, + }, + { + method: { name: "info", description: trans("toastComp.info"), params: params }, + execute: (comp, params) => { + showNotification(params, "info"); + }, + }, + { + method: { name: "success", description: trans("toastComp.success"), params: params }, + execute: (comp, params) => { + showNotification(params, "success"); + }, + }, + { + method: { name: "warn", description: trans("toastComp.warn"), params: params }, + execute: (comp, params) => { + showNotification(params, "warning"); + }, + }, + { + method: { name: "error", description: trans("toastComp.error"), params: params }, + execute: (comp, params) => { + showNotification(params, "error"); + }, + }, +]); + + diff --git a/client/packages/lowcoder/src/i18n/locales/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index cc41a8d1e..2b5e6c0a0 100644 --- a/client/packages/lowcoder/src/i18n/locales/de.ts +++ b/client/packages/lowcoder/src/i18n/locales/de.ts @@ -1612,10 +1612,18 @@ export const de = { }, "messageComp": { "info": "Eine Benachrichtigung senden", + "loading": "Ladebestätigung senden", "success": "Erfolgsbenachrichtigung senden", "warn": "Eine Warnmeldung senden", "error": "Eine Fehlerbenachrichtigung senden" }, + "tostComp": { + "info": "Eine Benachrichtigung senden", + "loading": "Ladebestätigung senden", + "success": "Erfolgsbenachrichtigung senden", + "warn": "Eine Warnmeldung senden", + "error": "Eine Fehlerbenachrichtigung senden" +}, "themeComp": { "switchTo": "Thema wechseln" }, diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 9e71926f0..dc1c9afe0 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1775,6 +1775,14 @@ export const en = { }, "messageComp": { "info": "Send a Notification", + "loading": "Send a Loading Notification", + "success": "Send a Success Notification", + "warn": "Send a Warning Notification", + "error": "Send an Error Notification" + }, + "toastComp": { + "info": "Send a Notification", + "loading": "Send a Loading Notification", "success": "Send a Success Notification", "warn": "Send a Warning Notification", "error": "Send an Error Notification" diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 12821ebf6..943cb2829 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -1683,6 +1683,14 @@ utilsComp: { }, messageComp: { info: "发送通知", + loading: "发送加载通知", + success: "发送成功通知", + warn: "发送警告通知", + error: "发送错误通知", +}, +toastComp: { + info: "发送通知", + loading: "发送加载通知", success: "发送成功通知", warn: "发送警告通知", error: "发送错误通知", diff --git a/docs/.gitbook/assets/builtin-js-toasts.png b/docs/.gitbook/assets/builtin-js-toasts.png new file mode 100644 index 0000000000000000000000000000000000000000..56d04f3fa858c470082f1e74ea7e5d126a2226a1 GIT binary patch literal 31319 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 B0Zjk^ literal 0 HcmV?d00001 diff --git a/docs/build-apps/write-javascript/built-in-javascript-functions.md b/docs/build-apps/write-javascript/built-in-javascript-functions.md index 757c30511..ee78acba1 100644 --- a/docs/build-apps/write-javascript/built-in-javascript-functions.md +++ b/docs/build-apps/write-javascript/built-in-javascript-functions.md @@ -109,18 +109,41 @@ utils.copyToClipboard( input1.value ) Use `message` methods to send a global alert notification, which displays at the top of the screen and lasts for 3 seconds by default. Each of the following four methods supports a unique display style. ```javascript -// messageInstance.info( text: string, options?: {duration: number = 3 } ) -messageInstance.info("Please confirm your information", { duration: 10 }) -// messageInstance.success( text: string, options?: {duration: number = 3 } ) -messageInstance.success("Query runs successfully", { duration: 10 }) -// messageInstance.warning( text: string, options?: {duration: number = 3 } ) -messageInstance.warning("Warning", { duration: 10 }) -// messageInstance.error( text: string, options?: {duration: number = 3 } ) -messageInstance.error("Query runs with error", { duration: 10 }) +// message.info( text: string, options?: {duration: number = 3 } ) +message.info("Please confirm your information", { duration: 10 }) +// message.loading( text: string, options?: {duration: number = 3 } ) +message.loading("Query is running", { duration: 5 }) +// message.success( text: string, options?: {duration: number = 3 } ) +message.success("Query runs successfully", { duration: 10 }) +// message.warning( text: string, options?: {duration: number = 3 } ) +message.warning("Warning", { duration: 10 }) +// message.error( text: string, options?: {duration: number = 3 } ) +message.error("Query runs with error", { duration: 10 }) ```

+## toast - dismissible stack-able notifications + +Use `toast` methods to send a notification, which displays at the top of the screen and lasts for 3 seconds by default. Each of the following five methods supports a unique display style. After 3 toasts they will be stacked. + +The id field can be used to update previous toasts. + +```javascript +// toast.open( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) +toast.open("This Is a Notification", {message: "I do not go away automatically.", duration: 0}) +// toast.info( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) +toast.info("Order #1519", {message: "Shipped out on Tuesday, Jan 3rd.", duration: 5}) +// toast.success( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) +toast.success("Query runs successfully", { duration: 10 }) +// toast.warn( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) +toast.warn("Duplicate Action", {message: "The email was previously sent on Jan 3rd. Click the button again to send.", duration: 5}) +// toast.error( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) +toast.error("Your credentials were invalid", {message: "You have 5 tries left", duration: 5}) +``` + +
+ ## localStorage Use `localStorage` methods to store and manage key-value pair data locally, which is not reset when the app refreshes, and can be accessed in any app within the workspace using `localStorage.values`. From 683179cd3167bfaf98d8569a3b4f66ca90a3e365 Mon Sep 17 00:00:00 2001 From: Andy C <46699959+sudoischenny@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:21:50 -0500 Subject: [PATCH 13/32] Update Toasts Image --- .vscode/settings.json | 7 +++++++ docs/.gitbook/assets/builtin-js-toasts.png | Bin 31319 -> 34438 bytes 2 files changed, 7 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..39649ae48 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "workbench.colorCustomizations": { + "activityBar.background": "#2A3012", + "titleBar.activeBackground": "#3B431A", + "titleBar.activeForeground": "#F9FAF2" + } +} \ No newline at end of file diff --git a/docs/.gitbook/assets/builtin-js-toasts.png b/docs/.gitbook/assets/builtin-js-toasts.png index 56d04f3fa858c470082f1e74ea7e5d126a2226a1..cfaf5833205312800645c4ddd6185e7e424c0f7a 100644 GIT binary patch literal 34438 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 z�Y-(}(RPM5ikoQ_+cC72pb9%tKjZM2VJ(JbcB-O1Q2&jZmulJo$Fc`q@4saOM<= zZb34;FU){TkvA)?K2NrK`{EsXyK8tnQ@AiEp-40&nmI{gNnEoO#YksjHg)RiM>$4s zp4wzpG`{LbFGOrJ2@-KbXS)u&^BqnK+5&lWMv+)AJBhV&Z5Z9Wd){1AZLR9X{Cl#N zmvIA6?QyTNMj(>Ucs2Tjk-Q!=Y}bg=r>s@$m9~0Cst*vJ^9;5u7_1BXeBKX3sW8|BsO;`#8^{*yw48VBREX{IMROE(VL znuyvmu9HQJEo+I>%_%XM(^goBdmmT2#l=kD;5a%?suYF1u7?Dhoti0GXqb)hI_JSg zkYapVQ4WmGa_TgAY%8MOAi7+XK-=rVMjj_S-#c-hw+j({qh6(6zG3$^ugQAd>1ke& zB>^lkV;o=3B*utIhrVg5mqx>uzvTeqJc~QgM$Pn?tOEOc;M+?N>l$D6A>$dt8XnzY zJR6irY$xAMXuzE|%JGS8t(N0#9LaAUc7qeBG4EuYzYCk<@-c#Z9o1E#RHItL-L%Y)Q3c@+yb4oTU?`G`! zg*Y--pNmA5i@@t!A};8yC^63W0_2)++vk$j2PEf@5vc zbcV7OK^g|$$*ZH))pjj*xjNU&s}2U;c`hE~k83i^g-(uWRI|%+jHF2eFO)=k&CZHN zFPpViEIqNki_lM$=cI3i8Vm=q*r&2PkT^U?f8x8PD(~|!;H!5f#OGtY+O`u(Gswra zO21Lq(NfdfDUdYdaQu`*Bz$g#!vg2d-IG*cpMgh0aS+@nZFj3GD<#J_-F;QneA_yq z7nwem_rG_n{%sfi7hp8`4`39iK^o;C|Ij)9C*(gXAy0FR7>kN7!UFz^A_(mE8$^|a zrXu~{6&zmz(YHY=N)n!o(W<1`X$i_&fKJ51yBxdAxvO$ag@XmH{`No@+cKg2 zF*@Pmly$*;HD10arc1Tl0GuZgxUfytzs>ku_zygoqvWz3V0w-R_j}TKb_^ z_DfKt@4wbvV=Lz~d}EN9kM~{|>%EEK6vbimES$}g<))XU!ZEZh@`%3cQed0#VWjY& z_=p4!I}@DL$T$k$&AnP!QKS13&*u{G&^?rf951k*qqrTp6+*Ve=k|7J&rzJGBsFKJ zo!BsE!t@B>pSS-n922gfZmf$zh8@dx_*zPUVVYKT3L%VBcJ%SMh^wP>c`BP5NITE- z;JZ`wB|wQ4lpc>1c(sO0%N~UmBi#FR$Xc^M{oylY2xge!rgt1|B;9mNt(Vr&RID2j9aV!lMV-fe;(wXc1?HRy^_Wkfv z?&!=*pwUMx_Eg&kv3h?HEb64jKrePhv%CQGompkk>1Rtt-*Rd5qhn345~Spv5wT-9 zq$BaO!7S~yL^yKui1k*g9kt+CU?_g^Oop|O{9!PvCbB$!q(4Ec{@nt5J#ba-WjH-l z6PNwQvQcaLO;o@I)riMbTEmCoLv+9>%=|d6z}7|c5Lzs6w?jX$xP!|!Qe*%%%d zqd^>ydqF)JXIF(zOebTcDsUIubi=WlJ|+t{-HQwqWZI z1jwS@)v8dW3FVjq!-^5pBQNHdv8PNgUk*Ix$!fv8UHHTg?_b$8s|+LRoO_PE(UCKT zja12+Qbu&5C8G9Y34FXPT=beE#oN{3RNw9DUys3YmL_=%1H*>2d zV|l7lXM?`?ncCJK*&{uGpxV3DJZLlm6 zn9%_n9KoJNpNI7kEqw1d+YVQ zPOtFn1?=4FI(J<{5pJ86$%^}l287upKb8A(P|9C318Lt5IE6YTTXKwTdlGfe$@C?x z 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%0AAm|K!IDP-;*~zT`LJmNj0QsK@u{E6?pa4TT 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 NzXu1Gvkf5N8F*}ys%fp3!3BU zIP$!2JQ~rWGY^JpD}>ltUIY^FlR9xno(*s)FD;Z?^$o}5;xeRj1fJqxou|+GecZlK z#YAAijJ`@(r{H&dp6h1Z4=iXmz0DI|_$kPxe6NTR#=6*?%AdG6e)>QPB^zCQr5t4l z56`GnMYk5KDBByiW>=rVXuE^;ex&f@^q!Y7Yw)`A8~-c5Rtwu zYpWqzB-(KFrWP&F;HQh^B+ubgNu@5n4tfRili&NJ}`L$Sg52s3yoQxk; zYdAm0tMOC4v*C{m+0p`e+%`_|^k24(LJDGDMLI9}N&Pe#bIaI)Y$uKBBXR}okptIf@gGFp2DGJa^8wYq= z;7Uh)VavC&Hp=l&_O+B9={2iD+jV>MSgq9FNQ%=jL9H#L4@4iS_pmD8VezM^H^WLz z4=h=Sv_7uo67kw|w{5_x&UMGKsO&~*zM9t{61yJu?FxYlfw&U4*3dscm;d;T{->Xf zg_hwb*WaeeJ80@e06XjtAqXbd!CGB&`_l4)KhvmmMvepbjgGrL1dg zGi}5+Fwp_kGAK<@A%#xMVA}2U+o`%`elp0TBy@}hEX^0&A^sxxiUx6w-fAGC8T1U} z)Plm~)XUTj&~MKRj|GR~oTlBw#XEO>n-__LSYmJT?U4|}>rII``xpp*Eck*)jsRcq zIZc`2?qzba!|g9c)fz+AI{Ir%Wuop~i_Y8KP;QWzyFGG9L4TPO3~5AH1sy|BP;J50 z=7O@We%y}8=e&RdjXdXLuh+jn1IJXw9794oC%7c2-?+0DljH}c`F-|&rBgldsV6Tx z{4_6 z(n%Qi8WIz9K2L{AI0fOxe>)~#312qzXf^(AR~;j+{{(9Mc{#;GR^zj6kVdk{VvyCW zEJn>#F&yx6k)LRJh2>mNo6e0=Q1;6;J^#GxmQV7jsu96N72h5Cf>-v)h?hB9Q+I!b z=dkuT7^a}=|8;J*TS_y_oPCmsm$2efRwQ+-OP!75r@KVymz4*dxtQ@bavzR_WYZ-l zvIhyfVm)0JpEZ0gM=QZ`&d$O7JCwJGa+N?YeFe%;LucGpFABF(p!nx`6?Fm4``s*B zaE5Oh$=7Nz?S@bN$#1OJ75qj0`hUsjA+_7N_nF}r)$sGHg+>3^%KyTHF2o)(sx4%j z!?ShhfI0AQ5zuA5gdiXQVkH6DKEF1DN#=f$=5Y`X@O4$KpG%sA4Ctk5R75@ObK*{aUSn|^DvGu|a2~?)~ z0KhqU0R9vF0?3t4_#d|bkPX48?(?QIF;VIhTpfVF&;$5O<^>aD=-H_mP}Un@7_{q< z>Z-Z`GTG81@XzVJ3%vG)2p~~3;y&2%0Pq_jARn*9smj=UX+HV4b2ptXf$;_4r58{A z2T%*=n2a++kUPV_F63W#U@KU@7KE)c-iT-iBoU`x9xGx74^yUU~VfMV+qdRTXA4di4|xt{A~wdb6_KoHPFz- zfnNZ%25gTre_b3B!~w0#-Js_g^YDt(!T-_6I3!5Gft`latE`4A&m!KSaaklRI}dP$ zvF-X|GE(Y~NuovE8OEL)L|#^z2M~LpYP*V7?l7$qZDLMhzWpsF8kTk;gos~dsvRM> zx~LXuqF2=~7XzW%q-SC8-)Lg1zBzik1$DS!#)FDZ-v HpQ--^9VU!B literal 31319 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 B0Zjk^ From 3708c3052158ff087a1f95f179b55ceb6fcfb9ae Mon Sep 17 00:00:00 2001 From: Andy C <46699959+sudoischenny@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:39:07 -0500 Subject: [PATCH 14/32] Added toast.destroy() Destroys a previous toast based on assigned id. --- .../lowcoder/src/comps/hooks/toastComp.ts | 37 +++++++------------ .../packages/lowcoder/src/i18n/locales/en.ts | 1 + .../built-in-javascript-functions.md | 6 ++- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp.ts b/client/packages/lowcoder/src/comps/hooks/toastComp.ts index 604c127ed..49174a784 100644 --- a/client/packages/lowcoder/src/comps/hooks/toastComp.ts +++ b/client/packages/lowcoder/src/comps/hooks/toastComp.ts @@ -37,36 +37,27 @@ const showNotification = ( text && notificationInstance[level](notificationArgs); }; +const destroy = ( + params: EvalParamType[] +) => { + // Extract the id from the params + const id = params[0] as React.Key; + + // Call notificationInstance.destroy with the provided id + notificationInstance.destroy(id); +}; + //what we would like to expose: title, text, duration, id, btn-obj, onClose, placement const ToastCompBase = simpleMultiComp({}); export let ToastComp = withExposingConfigs(ToastCompBase, []); -/* -export declare const NotificationPlacements: readonly ["top", "topLeft", "topRight", "bottom", "bottomLeft", "bottomRight"]; -export type NotificationPlacement = (typeof NotificationPlacements)[number]; -export type IconType = 'success' | 'info' | 'error' | 'warning'; -export interface ArgsProps { - message: React.ReactNode; - description?: React.ReactNode; - btn?: React.ReactNode; - key?: React.Key; - onClose?: () => void; - duration?: number | null; - icon?: React.ReactNode; - placement?: NotificationPlacement; - style?: React.CSSProperties; - className?: string; - readonly type?: IconType; - onClick?: () => void; - closeIcon?: React.ReactNode; - props?: DivProps; - role?: 'alert' | 'status'; -} -*/ - ToastComp = withMethodExposing(ToastComp, [ + { + method: { name: "destroy", description: trans("toastComp.destroy"), params: params }, + execute: (comp, params) => destroy(params), + }, { method: { name: "open", description: trans("toastComp.info"), params: params }, execute: (comp, params) => { diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index dc1c9afe0..4c0ddd264 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1781,6 +1781,7 @@ export const en = { "error": "Send an Error Notification" }, "toastComp": { + "destroy": "close a Notification", "info": "Send a Notification", "loading": "Send a Loading Notification", "success": "Send a Success Notification", diff --git a/docs/build-apps/write-javascript/built-in-javascript-functions.md b/docs/build-apps/write-javascript/built-in-javascript-functions.md index ee78acba1..aecebe6b8 100644 --- a/docs/build-apps/write-javascript/built-in-javascript-functions.md +++ b/docs/build-apps/write-javascript/built-in-javascript-functions.md @@ -127,7 +127,9 @@ message.error("Query runs with error", { duration: 10 }) Use `toast` methods to send a notification, which displays at the top of the screen and lasts for 3 seconds by default. Each of the following five methods supports a unique display style. After 3 toasts they will be stacked. -The id field can be used to update previous toasts. +The id field can be used to update previous toasts. Or used to destroy the previous toast. + +Destroy function used without an id will remove all toast. ```javascript // toast.open( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) @@ -140,6 +142,8 @@ toast.success("Query runs successfully", { duration: 10 }) toast.warn("Duplicate Action", {message: "The email was previously sent on Jan 3rd. Click the button again to send.", duration: 5}) // toast.error( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) toast.error("Your credentials were invalid", {message: "You have 5 tries left", duration: 5}) +//toast.destroy(id?: string) +toast.destroy() ```

From 791cff1e98d26386ce6aeda7bee8877639240cde Mon Sep 17 00:00:00 2001 From: Andy C <46699959+sudoischenny@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:02:40 -0500 Subject: [PATCH 15/32] Toast Dismiss Added the ability to allow or disallow toasts to be dismissed. --- client/packages/lowcoder/src/comps/hooks/toastComp.ts | 10 ++++++---- .../write-javascript/built-in-javascript-functions.md | 10 +++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/client/packages/lowcoder/src/comps/hooks/toastComp.ts b/client/packages/lowcoder/src/comps/hooks/toastComp.ts index 49174a784..8b80e89b9 100644 --- a/client/packages/lowcoder/src/comps/hooks/toastComp.ts +++ b/client/packages/lowcoder/src/comps/hooks/toastComp.ts @@ -19,17 +19,19 @@ const showNotification = ( const text = params?.[0] as string; const options = params?.[1] as JSONObject; - const { message , duration, id, placement } = options; + const { message , duration, id, placement, dismissible } = options; + + const closeIcon: boolean | undefined = dismissible === true ? undefined : (dismissible === false ? false : undefined); - // Convert duration to a number or null, if it's not a valid number, default to null const durationNumberOrNull: number | null = typeof duration === 'number' ? duration : null; const notificationArgs: ArgsProps = { message: text, description: message as React.ReactNode, duration: durationNumberOrNull ?? 3, - key: id as React.Key, // Ensure id is a valid React.Key - placement: placement as NotificationPlacement ?? "bottomRight", // Ensure placement is a valid NotificationPlacement or undefined + key: id as React.Key, + placement: placement as NotificationPlacement ?? "bottomRight", + closeIcon: closeIcon as boolean, }; // Use notificationArgs to trigger the notification diff --git a/docs/build-apps/write-javascript/built-in-javascript-functions.md b/docs/build-apps/write-javascript/built-in-javascript-functions.md index aecebe6b8..0bbbb6db0 100644 --- a/docs/build-apps/write-javascript/built-in-javascript-functions.md +++ b/docs/build-apps/write-javascript/built-in-javascript-functions.md @@ -132,15 +132,15 @@ The id field can be used to update previous toasts. Or used to destroy the previ Destroy function used without an id will remove all toast. ```javascript -// toast.open( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) +// toast.open( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight", dismissible?: boolean = true } ) toast.open("This Is a Notification", {message: "I do not go away automatically.", duration: 0}) -// toast.info( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) +// toast.info( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight", dismissible?: boolean = true } ) toast.info("Order #1519", {message: "Shipped out on Tuesday, Jan 3rd.", duration: 5}) -// toast.success( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) +// toast.success( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight", dismissible?: boolean = true } ) toast.success("Query runs successfully", { duration: 10 }) -// toast.warn( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) +// toast.warn( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight", dismissible?: boolean = true } ) toast.warn("Duplicate Action", {message: "The email was previously sent on Jan 3rd. Click the button again to send.", duration: 5}) -// toast.error( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight" } ) +// toast.error( title: string, options?: { message?: string, duration?: number = 3, id?: string, placement?: "top" | "topLeft" | "topRight" | "bottom" | "bottomRight", "bottomRight" = "bottomRight", dismissible?: boolean = true } ) toast.error("Your credentials were invalid", {message: "You have 5 tries left", duration: 5}) //toast.destroy(id?: string) toast.destroy() From 9857111c5a1d4da2db19833a6404b2e2453d99fb Mon Sep 17 00:00:00 2001 From: Abdul Qadir Date: Wed, 28 Feb 2024 14:30:07 +0500 Subject: [PATCH 16/32] Add migration to fix existing application public view bug --- .../domain/application/model/Application.java | 11 +++++-- .../service/ApplicationService.java | 8 +++++ .../runner/migrations/DatabaseChangelog.java | 6 ++++ .../migrations/job/AddPtmFieldsJob.java | 6 ++++ .../migrations/job/AddPtmFieldsJobImpl.java | 29 +++++++++++++++++++ 5 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddPtmFieldsJob.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddPtmFieldsJobImpl.java 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 89c76c41b..332464894 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 @@ -12,6 +12,7 @@ import java.util.Set; import java.util.function.Supplier; +import lombok.Setter; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.lowcoder.domain.query.model.ApplicationQuery; @@ -38,10 +39,13 @@ public class Application extends HasIdAndAuditing { private final Map publishedApplicationDSL; - private final Boolean publicToAll; - private final Boolean publicToMarketplace; + @Setter + private Boolean publicToAll; + @Setter + private Boolean publicToMarketplace; - private final Boolean agencyProfile; + @Setter + private Boolean agencyProfile; private Map editingApplicationDSL; @@ -161,4 +165,5 @@ public Map getEditingApplicationDSL() { public Object getLiveContainerSize() { return liveContainerSize.get(); } + } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java index 917d0762e..06ab6287e 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java @@ -66,6 +66,10 @@ public Mono updateById(String applicationId, Application application) { return Mono.error(new BizException(BizError.INVALID_PARAMETER, "INVALID_PARAMETER", FieldName.ID)); } + log.info("inside mongoUpsertHelper "); + + log.info("application: " + application); + return mongoUpsertHelper.updateById(application, applicationId); } @@ -220,4 +224,8 @@ public Mono> getPublicApplicationIds(Collection applicationI } + + public Flux findAll() { + return repository.findAll(); + } } 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 90d62e98b..6e33d075b 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 @@ -18,6 +18,7 @@ import org.lowcoder.infra.config.model.ServerConfig; import org.lowcoder.infra.eventlog.EventLog; import org.lowcoder.infra.serverlog.ServerLog; +import org.lowcoder.runner.migrations.job.AddPtmFieldsJob; import org.lowcoder.runner.migrations.job.CompleteAuthType; import org.lowcoder.runner.migrations.job.MigrateAuthConfigJob; import org.springframework.data.domain.Sort; @@ -182,6 +183,11 @@ public void addOrgIdIndexOnServerLog(MongockTemplate mongoTemplate) { ); } + @ChangeSet(order = "020", id = "add-ptm-fields-to-applications", author = "") + public void addPtmFieldsToApplicatgions(AddPtmFieldsJob addPtmFieldsJob) { + addPtmFieldsJob.migrateApplicationsToInitPtmFields(); + } + public static Index makeIndex(String... fields) { if (fields.length == 1) { return new Index(fields[0], Sort.Direction.ASC).named(fields[0]); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddPtmFieldsJob.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddPtmFieldsJob.java new file mode 100644 index 000000000..44b492f75 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddPtmFieldsJob.java @@ -0,0 +1,6 @@ +package org.lowcoder.runner.migrations.job; + +public interface AddPtmFieldsJob { + + void migrateApplicationsToInitPtmFields(); +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddPtmFieldsJobImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddPtmFieldsJobImpl.java new file mode 100644 index 000000000..b4701d710 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddPtmFieldsJobImpl.java @@ -0,0 +1,29 @@ +package org.lowcoder.runner.migrations.job; + +import org.lowcoder.domain.application.service.ApplicationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class AddPtmFieldsJobImpl implements AddPtmFieldsJob { + + @Autowired + private ApplicationService applicationService; + + @Override + public void migrateApplicationsToInitPtmFields() { + applicationService.findAll() + .doOnNext(application -> { + if(!application.isPublicToAll()) { + application.setPublicToAll(Boolean.FALSE); + } + if(!application.isPublicToMarketplace()) { + application.setPublicToMarketplace(Boolean.FALSE); + } + if(!application.agencyProfile()) { + application.setAgencyProfile(Boolean.FALSE); + } + }).flatMap(application -> applicationService.updateById(application.getId(), application)) + .blockLast(); + } +} From e5bbd357a52e2c6770e5e473062d779f923653b3 Mon Sep 17 00:00:00 2001 From: Abdul Qadir Date: Wed, 28 Feb 2024 14:40:56 +0500 Subject: [PATCH 17/32] Remove unnecessary logs --- .../domain/application/service/ApplicationService.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java index 06ab6287e..dfc3f2a8d 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java @@ -66,10 +66,6 @@ public Mono updateById(String applicationId, Application application) { return Mono.error(new BizException(BizError.INVALID_PARAMETER, "INVALID_PARAMETER", FieldName.ID)); } - log.info("inside mongoUpsertHelper "); - - log.info("application: " + application); - return mongoUpsertHelper.updateById(application, applicationId); } From a85f478a0a37180296c81342faf0e8d92d80aa4f Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Wed, 28 Feb 2024 16:20:38 +0500 Subject: [PATCH 18/32] fixed styledcomponent warning in drawerComp --- .../lowcoder/src/comps/hooks/drawerComp.tsx | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx b/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx index 1d04e984d..2cfb5e1ca 100644 --- a/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx @@ -35,6 +35,35 @@ const DrawerWrapper = styled.div` pointer-events: auto; `; +const ButtonStyle = styled(Button)<{$closePosition?: string}>` + position: absolute; + ${(props) => props.$closePosition === "right" ? "right: 0;" : "left: 0;"} + top: 0; + z-index: 10; + font-weight: 700; + box-shadow: none; + color: rgba(0, 0, 0, 0.45); + height: 54px; + width: 54px; + + svg { + width: 16px; + height: 16px; + } + + &, + :hover, + :focus { + background-color: transparent; + border: none; + } + + :hover, + :focus { + color: rgba(0, 0, 0, 0.75); + } +`; + // If it is a number, use the px unit by default function transToPxSize(size: string | number) { return isNumeric(size) ? size + "px" : (size as string); @@ -103,34 +132,6 @@ let TmpDrawerComp = (function () { }, [dispatch, isTopBom] ); - const ButtonStyle = styled(Button)` - position: absolute; - ${props.closePosition === "right" ? "right: 0;" : "left: 0;"} - top: 0; - z-index: 10; - font-weight: 700; - box-shadow: none; - color: rgba(0, 0, 0, 0.45); - height: 54px; - width: 54px; - - svg { - width: 16px; - height: 16px; - } - - &, - :hover, - :focus { - background-color: transparent; - border: none; - } - - :hover, - :focus { - color: rgba(0, 0, 0, 0.75); - } - `; return ( @@ -168,6 +169,7 @@ let TmpDrawerComp = (function () { mask={props.showMask} > { props.visible.onChange(false); }} From 08b675a233fe2ee0debd3fb6e11f3fe90fd74f83 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Wed, 28 Feb 2024 16:20:57 +0500 Subject: [PATCH 19/32] added app meta fields --- .../src/comps/comps/appSettingsComp.tsx | 88 ++++++++++++++++--- .../packages/lowcoder/src/i18n/locales/de.ts | 5 +- .../packages/lowcoder/src/i18n/locales/en.ts | 5 +- .../packages/lowcoder/src/i18n/locales/zh.ts | 3 + .../src/pages/common/styledComponent.tsx | 1 + .../src/pages/editor/styledComponents.tsx | 2 +- 6 files changed, 91 insertions(+), 13 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx index 9fa023cae..54c294ea0 100644 --- a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx @@ -1,7 +1,7 @@ import { ThemeDetail, ThemeType } from "api/commonSettingApi"; import { RecordConstructorToComp } from "lowcoder-core"; import { dropdownInputSimpleControl } from "comps/controls/dropdownInputSimpleControl"; -import { MultiCompBuilder, valueComp } from "comps/generators"; +import { MultiCompBuilder, valueComp, withDefault } from "comps/generators"; import { AddIcon, Dropdown } from "lowcoder-design"; import { EllipsisSpan } from "pages/setting/theme/styledComponents"; import { useEffect } from "react"; @@ -14,6 +14,10 @@ import { default as Divider } from "antd/es/divider"; import { THEME_SETTING } from "constants/routesURL"; import { CustomShortcutsComp } from "./customShortcutsComp"; import { DEFAULT_THEMEID } from "comps/utils/themeUtil"; +import { StringControl } from "comps/controls/codeControl"; +import { IconControl } from "comps/controls/iconControl"; +import { dropdownControl } from "comps/controls/dropdownControl"; +import { ApplicationCategoriesEnum } from "constants/applicationConstants"; const TITLE = trans("appSetting.title"); const USER_DEFINE = "__USER_DEFINE"; @@ -92,9 +96,37 @@ const SettingsStyled = styled.div` `; const DivStyled = styled.div` - div { - width: 100%; - display: block; + > div { + flex-wrap: wrap; + margin-bottom: 12px; + + > div { + width: 100%; + display: block; + } + + > div:first-child { + margin-bottom: 6px; + } + + .tooltipLabel { + white-space: nowrap; + } + + } + // custom styles for icon selector + .app-icon { + > div { + margin-bottom: 0; + + > div:first-child { + margin-bottom: 6px; + } + > div:nth-child(2) { + width: 88%; + display: inline-block; + } + } } `; @@ -134,7 +166,22 @@ const DividerStyled = styled(Divider)` border-color: #e1e3eb; `; +type AppCategoriesEnumKey = keyof typeof ApplicationCategoriesEnum +const AppCategories = Object.keys(ApplicationCategoriesEnum).map( + (cat) => { + const value = ApplicationCategoriesEnum[cat as AppCategoriesEnumKey]; + return { + label: value, + value, + } + } +) + const childrenMap = { + title: withDefault(StringControl, ''), + description: withDefault(StringControl, ''), + icon: IconControl, + category: dropdownControl(AppCategories, ApplicationCategoriesEnum.BUSINESS), maxWidth: dropdownInputSimpleControl(OPTIONS, USER_DEFINE, "1920"), themeId: valueComp(DEFAULT_THEMEID), customShortcuts: CustomShortcutsComp, @@ -146,7 +193,16 @@ type ChildrenInstance = RecordConstructorToComp & { }; function AppSettingsModal(props: ChildrenInstance) { - const { themeList, defaultTheme, themeId, maxWidth } = props; + const { + themeList, + defaultTheme, + themeId, + maxWidth, + title, + description, + icon, + category, + } = props; const THEME_OPTIONS = themeList?.map((theme) => ({ label: theme.name, value: theme.id + "", @@ -182,6 +238,23 @@ function AppSettingsModal(props: ChildrenInstance) { {TITLE} + {title.propertyView({ + label: trans("appSetting.appTitle"), + placeholder: trans("appSetting.appTitle") + })} + {description.propertyView({ + label: trans("appSetting.appDescription"), + placeholder: trans("appSetting.appDescription") + })} + {category.propertyView({ + label: trans("appSetting.appCategory"), + })} +
+ {icon.propertyView({ + label: trans("icon"), + tooltip: trans("aggregation.iconTooltip"), + })} +
{maxWidth.propertyView({ dropdownLabel: trans("appSetting.canvasMaxWidth"), inputLabel: trans("appSetting.userDefinedMaxWidth"), @@ -189,9 +262,6 @@ function AppSettingsModal(props: ChildrenInstance) { placement: "bottom", min: 350, lastNode: {trans("appSetting.maxWidthTip")}, - labelStyle: {marginBottom: "8px"}, - dropdownStyle: {marginBottom: "12px"}, - inputStyle: {marginBottom: "12px"} })} } preNode={() => ( <> diff --git a/client/packages/lowcoder/src/i18n/locales/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index cc41a8d1e..5c310d61d 100644 --- a/client/packages/lowcoder/src/i18n/locales/de.ts +++ b/client/packages/lowcoder/src/i18n/locales/de.ts @@ -1849,7 +1849,10 @@ export const de = { "maxWidthTip": "Die maximale Breite sollte größer als oder gleich 350 sein", "themeSetting": "Angewandter Stil Thema", "themeSettingDefault": "Standard", - "themeCreate": "Thema erstellen" + "themeCreate": "Thema erstellen", + "appTitle": "Titel", + "appDescription": "Beschreibung", + "appCategory": "Kategorie", }, "customShortcut": { "title": "Benutzerdefinierte Abkürzungen", diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 9e71926f0..fe8d2342f 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -2033,7 +2033,10 @@ export const en = { "maxWidthTip": "Max Width Should Be Greater Than or Equal to 350", "themeSetting": "Applied Style Theme", "themeSettingDefault": "Default", - "themeCreate": "Create Theme" + "themeCreate": "Create Theme", + "appTitle": "Title", + "appDescription": "Description", + "appCategory": "Category", }, "customShortcut": { "title": "Custom Shortcuts", diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 12821ebf6..67cc344e3 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -1922,6 +1922,9 @@ appSetting: { themeSetting: "主题设置", themeSettingDefault: "默认", themeCreate: "创建主题", + appTitle: "标题", + appDescription: "描述", + appCategory: "类别", }, customShortcut: { title: "自定义快捷键", diff --git a/client/packages/lowcoder/src/pages/common/styledComponent.tsx b/client/packages/lowcoder/src/pages/common/styledComponent.tsx index a119953c1..95fbb3cfe 100644 --- a/client/packages/lowcoder/src/pages/common/styledComponent.tsx +++ b/client/packages/lowcoder/src/pages/common/styledComponent.tsx @@ -39,6 +39,7 @@ const RightStyledCard = styled(Card)` export const LeftPanel = styled(StyledCard)` display: block; + z-index: ${Layers.rightPanel}; `; export const RightPanelWrapper = styled(RightStyledCard)` display: flex; diff --git a/client/packages/lowcoder/src/pages/editor/styledComponents.tsx b/client/packages/lowcoder/src/pages/editor/styledComponents.tsx index b1125d4c9..d8cb3a4a0 100644 --- a/client/packages/lowcoder/src/pages/editor/styledComponents.tsx +++ b/client/packages/lowcoder/src/pages/editor/styledComponents.tsx @@ -148,6 +148,6 @@ export const CollapseWrapper = styled.div<{ $clientX?: number }>` display: none; } .simplebar-content > div { - padding: 0; + // padding: 0; } `; From f30fdf2deee7a68e1c4fe75ace5ca8492ff202e9 Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Wed, 28 Feb 2024 21:03:37 +0500 Subject: [PATCH 20/32] TS issue fixed --- .../lowcoder/src/comps/controls/styleControlConstants.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index e36cd9d9b..d64daa8ea 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -798,7 +798,7 @@ export const MultiSelectStyle = [ ...ACCENT_VALIDATE, ] as const; -export const TabContainerStyle = [ +export const TabContainerStyle:any[] = [ // Keep background related properties of container as STYLING_FIELDS_SEQUENCE has rest of the properties ...replaceAndMergeMultipleStyles([...ContainerStyle.filter((style)=> ['border','radius','borderWidth','margin','padding'].includes(style.name) === false),...STYLING_FIELDS_SEQUENCE], 'text', [{ name: "tabText", From 6e816675d17b58a8794e1146ce08666850dee5fb Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Wed, 28 Feb 2024 21:14:00 +0500 Subject: [PATCH 21/32] Tab container type removal, due to compiler error --- .../lowcoder/src/comps/controls/styleControlConstants.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index d64daa8ea..e36cd9d9b 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -798,7 +798,7 @@ export const MultiSelectStyle = [ ...ACCENT_VALIDATE, ] as const; -export const TabContainerStyle:any[] = [ +export const TabContainerStyle = [ // Keep background related properties of container as STYLING_FIELDS_SEQUENCE has rest of the properties ...replaceAndMergeMultipleStyles([...ContainerStyle.filter((style)=> ['border','radius','borderWidth','margin','padding'].includes(style.name) === false),...STYLING_FIELDS_SEQUENCE], 'text', [{ name: "tabText", From b6e1e2c7fa6a6f9ce63336ab38cdf6b82a5c8352 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Thu, 29 Feb 2024 10:03:57 +0100 Subject: [PATCH 22/32] Added Comments to Marketplace Use --- .../domain/application/model/Application.java | 9 ++--- .../repository/ApplicationRepository.java | 3 ++ .../service/ApplicationService.java | 14 +++++--- .../application/ApplicationApiService.java | 25 +++++++++---- .../api/application/ApplicationEndpoints.java | 35 +++++-------------- .../application/view/ApplicationInfoView.java | 2 +- .../view/ApplicationPermissionView.java | 5 +++ .../api/home/UserHomeApiServiceImpl.java | 3 +- 8 files changed, 53 insertions(+), 43 deletions(-) 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 332464894..c102c5f55 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 @@ -43,7 +43,6 @@ public class Application extends HasIdAndAuditing { private Boolean publicToAll; @Setter private Boolean publicToMarketplace; - @Setter private Boolean agencyProfile; @@ -77,15 +76,17 @@ public class Application extends HasIdAndAuditing { @Builder @JsonCreator - public Application(@JsonProperty("orgId") String organizationId, + public Application( + @JsonProperty("orgId") String organizationId, @JsonProperty("name") String name, @JsonProperty("applicationType") Integer applicationType, @JsonProperty("applicationStatus") ApplicationStatus applicationStatus, @JsonProperty("publishedApplicationDSL") Map publishedApplicationDSL, + @JsonProperty("editingApplicationDSL") Map editingApplicationDSL, @JsonProperty("publicToAll") Boolean publicToAll, @JsonProperty("publicToMarketplace") Boolean publicToMarketplace, - @JsonProperty("agencyProfile") Boolean agencyProfile, - @JsonProperty("editingApplicationDSL") Map editingApplicationDSL) { + @JsonProperty("agencyProfile") Boolean agencyProfile + ) { this.organizationId = organizationId; this.name = name; this.applicationType = applicationType; 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 7c42ccc73..c6b2951fd 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 @@ -34,6 +34,9 @@ public interface ApplicationRepository extends ReactiveMongoRepository findByIdIn(List ids); + + // Falk: Why to combine? Marketplace-List and Agency-List are different Endpoints + @Query(value = "{$and:[{'publicToAll':true},{'$or':[{'publicToMarketplace':?0},{'agencyProfile':?1}]}, {'_id': { $in: ?2}}]}", fields = "{_id : 1}") Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsOrAgencyProfileIsAndIdIn (Boolean publicToMarketplace, Boolean agencyProfile, Collection ids); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java index dfc3f2a8d..34b24f1e2 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java @@ -152,13 +152,19 @@ public Mono setApplicationPublicToAll(String applicationId, boolean pub return mongoUpsertHelper.updateById(application, applicationId); } - public Mono setApplicationPublicToMarketplace(String applicationId, Boolean publicToMarketplace, - String title, String category, String description, String image) { + // Falk: String title, String category, String description, String image will be set in Application Settings inside DSL by Frontend + public Mono setApplicationPublicToMarketplace(String applicationId, Boolean publicToMarketplace) { return findById(applicationId) + + // Falk: question - do we need Map applicationDsl = application.getEditingApplicationDSL(); and .editingApplicationDSL(applicationDsl) - or is .publicToMarketplace(publicToMarketplace).build(); enough? + .map(application -> { + Map applicationDsl = application.getEditingApplicationDSL(); - if (applicationDsl.containsKey("ui")) { + + // Falk: this logic is not needed anymore, because we set Meta Data in Settings in the UI already + /* if (applicationDsl.containsKey("ui")) { Map dataObject = (Map) applicationDsl.get("ui"); if(publicToMarketplace) { @@ -178,7 +184,7 @@ public Mono setApplicationPublicToMarketplace(String applicationId, Boo applicationDsl.replace("ui", dataObject); - } + } */ return Application.builder() .publicToMarketplace(publicToMarketplace) 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 e43e48fba..6ced87d03 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 @@ -250,13 +250,20 @@ private Mono checkApplicationStatus(Application application, ApplicationSt private Mono checkApplicationViewRequest(Application application, ApplicationEndpoints.ApplicationRequestType expected) { // TODO: The check is correct ( logically ) but we need to provide some time for the users to adapt. Will bring it back in the next release - if (expected == ApplicationEndpoints.ApplicationRequestType.PUBLIC_TO_ALL /* && application.isPublicToAll() */) { - return Mono.empty(); - } - if (expected == ApplicationEndpoints.ApplicationRequestType.PUBLIC_TO_MARKETPLACE && application.isPublicToMarketplace()) { + + // Falk: switched && application.isPublicToAll() on again - seems here is the bug. + if (expected == ApplicationEndpoints.ApplicationRequestType.PUBLIC_TO_ALL && application.isPublicToAll()) { return Mono.empty(); } - if (expected == ApplicationEndpoints.ApplicationRequestType.AGENCY_PROFILE && application.agencyProfile()) { + + // Falk: here is to check the ENV Variable LOWCODER_MARKETPLACE_PRIVATE_MODE + // isPublicToMarketplace & isPublicToAll must be both true + if (expected == ApplicationEndpoints.ApplicationRequestType.PUBLIC_TO_MARKETPLACE && application.isPublicToMarketplace() && application.isPublicToAll()) { + return Mono.empty(); + } + // + // Falk: application.agencyProfile() & isPublicToAll must be both true + if (expected == ApplicationEndpoints.ApplicationRequestType.AGENCY_PROFILE && application.agencyProfile() && application.isPublicToAll()) { return Mono.empty(); } return Mono.error(new BizException(BizError.UNSUPPORTED_OPERATION, "BAD_REQUEST")); @@ -445,6 +452,7 @@ public Mono getApplicationPermissions(String applicat .orgName(organization.getName()) .publicToAll(application.isPublicToAll()) .publicToMarketplace(application.isPublicToMarketplace()) + .agencyProfile(application.agencyProfile()) .build(); }); }); @@ -502,6 +510,7 @@ private ApplicationInfoView buildView(Application application, String role, @Nul .folderId(folderId) .publicToAll(application.isPublicToAll()) .publicToMarketplace(application.isPublicToMarketplace()) + .agencyProfile(application.agencyProfile()) .build(); } @@ -519,13 +528,15 @@ public Mono setApplicationPublicToMarketplace(String applicationId, App return checkCurrentUserApplicationPermission(applicationId, ResourceAction.SET_APPLICATIONS_PUBLIC_TO_MARKETPLACE) .then(checkApplicationStatus(applicationId, NORMAL)) .then(applicationService.setApplicationPublicToMarketplace - (applicationId, request.publicToMarketplace(), request.title(), request.category(), request.description(), request.image())); + (applicationId, request.publicToMarketplace())); } + // Falk: why we have request.publicToMarketplace() - but here only agencyProfile? Not from request? public Mono setApplicationAsAgencyProfile(String applicationId, boolean agencyProfile) { return checkCurrentUserApplicationPermission(applicationId, ResourceAction.SET_APPLICATIONS_AS_AGENCY_PROFILE) .then(checkApplicationStatus(applicationId, NORMAL)) - .then(applicationService.setApplicationAsAgencyProfile(applicationId, agencyProfile)); + .then(applicationService.setApplicationAsAgencyProfile + (applicationId, agencyProfile)); } private Map sanitizeDsl(Map applicationDsl) { 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 2ac323289..4df112274 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 @@ -11,6 +11,9 @@ import org.lowcoder.api.application.view.ApplicationPermissionView; import org.lowcoder.api.application.view.ApplicationView; import org.lowcoder.api.application.view.MarketplaceApplicationInfoView; + +//Falk: shouldn't be here ...? +// import org.lowcoder.api.application.view.AgencyProfileApplicationView; import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.api.home.UserHomepageView; import org.lowcoder.domain.application.model.Application; @@ -115,7 +118,7 @@ public interface ApplicationEndpoints tags = TAG_APPLICATION_MANAGEMENT, operationId = "getMarketplaceApplicationDataInViewMode", summary = "Get Marketplace Application data in view mode", - description = "Retrieve the DSL data of a Lowcoder Application in view-mode by its ID for the marketplace." + description = "Retrieve the DSL data of a Lowcoder Application in view-mode by its ID for the Marketplace." ) @GetMapping("/{applicationId}/view_marketplace") public Mono> getPublishedMarketPlaceApplication(@PathVariable String applicationId); @@ -124,7 +127,7 @@ public interface ApplicationEndpoints tags = TAG_APPLICATION_MANAGEMENT, operationId = "getAgencyProfileApplicationDataInViewMode", summary = "Get Agency profile Application data in view mode", - description = "Retrieve the DSL data of a Lowcoder Application in view-mode by its ID marked as agency profile." + description = "Retrieve the DSL data of a Lowcoder Application in view-mode by its ID marked as Agency Profile." ) @GetMapping("/{applicationId}/view_agency") public Mono> getAgencyProfileApplication(@PathVariable String applicationId); @@ -171,12 +174,13 @@ public Mono>> getApplications(@RequestPar @Operation( tags = TAG_APPLICATION_MANAGEMENT, operationId = "listMarketplaceApplications", - summary = "List marketplace Applications", - description = "Retrieve a list of Lowcoder Applications that are published to the marketplace" + summary = "List Marketplace Applications", + description = "Retrieve a list of Lowcoder Applications that are published to the Marketplace" ) @GetMapping("/marketplace-apps") public Mono>> getMarketplaceApplications(@RequestParam(required = false) Integer applicationType); + // Falk: why we use MarketplaceApplicationInfoView for AgencyProfile? @Operation( tags = TAG_APPLICATION_MANAGEMENT, operationId = "listAgencyProfileApplications", @@ -270,33 +274,12 @@ public Boolean publicToAll() { } } - public record ApplicationPublicToMarketplaceRequest(Boolean publicToMarketplace, String title, - String description, String category, String image) { + public record ApplicationPublicToMarketplaceRequest(Boolean publicToMarketplace) { @Override public Boolean publicToMarketplace() { return BooleanUtils.isTrue(publicToMarketplace); } - @Override - public String title() { - return title; - } - - @Override - public String description() { - return description; - } - - @Override - public String category() { - return category; - } - - @Override - public String image() { - return image; - } - } public record ApplicationAsAgencyProfileRequest(Boolean agencyProfile) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationInfoView.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationInfoView.java index 66eda1871..ca7702a26 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationInfoView.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationInfoView.java @@ -37,8 +37,8 @@ public class ApplicationInfoView { private final Instant lastModifyTime; // app's last update time private final boolean publicToAll; - private final boolean publicToMarketplace; + private final boolean agencyProfile; public long getLastViewTime() { return lastViewTime == null ? 0 : lastViewTime.toEpochMilli(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationPermissionView.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationPermissionView.java index ec17670bf..f5659eba6 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationPermissionView.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/view/ApplicationPermissionView.java @@ -9,6 +9,7 @@ public class ApplicationPermissionView extends CommonPermissionView { private boolean publicToAll; private boolean publicToMarketplace; + private boolean agencyProfile; public boolean isPublicToAll() { return publicToAll; @@ -17,4 +18,8 @@ public boolean isPublicToAll() { public boolean isPublicToMarketplace() { return publicToMarketplace; } + + public boolean isAgencyProfile() { + return agencyProfile; + } } 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 2662900dd..95f62de61 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 @@ -399,7 +399,8 @@ private ApplicationInfoView buildView(Application application, ResourceRole maxR .lastModifyTime(application.getUpdatedAt()) .lastViewTime(lastViewTime) .publicToAll(application.isPublicToAll()) - .publicToMarketplace(application.isPublicToMarketplace()); + .publicToMarketplace(application.isPublicToMarketplace()) + .agencyProfile(application.agencyProfile()); if (withContainerSize) { return applicationInfoViewBuilder .containerSize(application.getLiveContainerSize()) From 877f9f525262712a82c83503c3cb726a3b44bded Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Thu, 29 Feb 2024 11:47:04 +0100 Subject: [PATCH 23/32] Add Support as App Category --- client/packages/lowcoder/src/constants/applicationConstants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/packages/lowcoder/src/constants/applicationConstants.ts b/client/packages/lowcoder/src/constants/applicationConstants.ts index 2e09ba8c3..e947676cc 100644 --- a/client/packages/lowcoder/src/constants/applicationConstants.ts +++ b/client/packages/lowcoder/src/constants/applicationConstants.ts @@ -16,6 +16,7 @@ export enum AppTypeEnum { } export enum ApplicationCategoriesEnum { + SUPPORT = "Support", BUSINESS = "Business", DASHBOARD = "Dashboards & Reporting", SLIDES = "Slides & Presentations", From 4f58b9620a645ae562cc44227b0c4fca9775ea5d Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Fri, 1 Mar 2024 20:37:47 +0100 Subject: [PATCH 24/32] Adding Admin Area Icons --- .../lowcoder-design/src/icons/index.ts | 33 +++++----- .../src/components/layout/SubSideBar.tsx | 5 +- .../packages/lowcoder/src/i18n/locales/en.ts | 4 +- .../packages/lowcoder/src/i18n/locales/zh.ts | 4 +- .../src/pages/ApplicationV2/index.tsx | 64 +++---------------- .../src/pages/setting/settingHome.tsx | 44 ++++++++----- 6 files changed, 63 insertions(+), 91 deletions(-) diff --git a/client/packages/lowcoder-design/src/icons/index.ts b/client/packages/lowcoder-design/src/icons/index.ts index 6fdc18332..99c7b553c 100644 --- a/client/packages/lowcoder-design/src/icons/index.ts +++ b/client/packages/lowcoder-design/src/icons/index.ts @@ -171,22 +171,6 @@ export { ReactComponent as videoPlayTriangle } from "./icon-video-play-triangle. export { ReactComponent as DrawerCompIcon } from "./icon-drawer.svg"; export { ReactComponent as LeftMeetingIcon } from "./icon-left-comp-video.svg"; export { ReactComponent as PlusIcon } from "./icon-plus.svg"; -export { ReactComponent as HomeIcon } from "./icon-application-home.svg"; -export { ReactComponent as HomeModuleIcon } from "./icon-application-module.svg"; -export { ReactComponent as HomeQueryLibraryIcon } from "./icon-application-query-library.svg"; -export { ReactComponent as HomeDataSourceIcon } from "./icon-application-datasource.svg"; -export { ReactComponent as RecyclerIcon } from "./icon-application-recycler.svg"; -export { ReactComponent as MarketplaceIcon } from "./icon-application-marketplace.svg"; -export { ReactComponent as LowcoderMarketplaceIcon } from "./icon-lowcoder-marketplace.svg"; -export { ReactComponent as HomeActiveIcon } from "./icon-application-home-active.svg"; -export { ReactComponent as HomeModuleActiveIcon } from "./icon-application-module-active.svg"; -export { ReactComponent as HomeQueryLibraryActiveIcon } from "./icon-application-query-library-active.svg"; -export { ReactComponent as HomeDataSourceActiveIcon } from "./icon-application-datasource-active.svg"; -export { ReactComponent as RecyclerActiveIcon } from "./icon-application-recycler-active.svg"; -export { ReactComponent as MarketplaceActiveIcon } from "./icon-application-marketplace-active.svg"; -export { ReactComponent as LowcoderMarketplaceActiveIcon } from "./icon-lowcoder-marketplace-active.svg"; -export { ReactComponent as FavoritesIcon } from "./icon-application-favorites.svg"; -export { ReactComponent as HomeSettingIcon } from "./icon-application-setting.svg"; export { ReactComponent as FolderIcon } from "./icon-application-folder.svg"; export { ReactComponent as AllTypesIcon } from "./icon-application-all.svg"; export { ReactComponent as InviteUserIcon } from "./icon-application-invite-user.svg"; @@ -314,6 +298,23 @@ export { ReactComponent as LeftShow } from "./remix/eye-off-line.svg"; export { ReactComponent as LeftHide } from "./remix/eye-line.svg"; export { ReactComponent as LeftLock } from "./remix/lock-line.svg"; export { ReactComponent as LeftUnlock } from "./remix/lock-unlock-line.svg"; +export { ReactComponent as UserGroupIcon } from "./remix/group-line.svg"; +export { ReactComponent as UserIcon } from "./remix/user-line.svg"; +export { ReactComponent as UserAddIcon } from "./remix/user-add-line.svg"; +export { ReactComponent as UserDeleteIcon } from "./remix/user-unfollow-line.svg"; +export { ReactComponent as UserShieldIcon } from "./remix/shield-user-line.svg"; +export { ReactComponent as ThemeIcon } from "./remix/palette-line.svg"; +export { ReactComponent as AppsIcon } from "./remix/apps-2-line.svg"; +export { ReactComponent as WorkspacesIcon } from "./remix/hotel-line.svg"; + +export { ReactComponent as HomeIcon } from "./remix/home-3-line.svg"; +export { ReactComponent as HomeModuleIcon } from "./remix/focus-mode.svg"; +export { ReactComponent as HomeQueryLibraryIcon } from "./remix/braces-line.svg"; +export { ReactComponent as HomeDataSourceIcon } from "./remix/database-2-line.svg"; +export { ReactComponent as RecyclerIcon } from "./remix/delete-bin-line.svg"; +export { ReactComponent as MarketplaceIcon } from "./icon-application-marketplace.svg"; +export { ReactComponent as FavoritesIcon } from "./icon-application-favorites.svg"; +export { ReactComponent as HomeSettingIcon } from "./remix/settings-4-line.svg"; // new diff --git a/client/packages/lowcoder/src/components/layout/SubSideBar.tsx b/client/packages/lowcoder/src/components/layout/SubSideBar.tsx index c8af9a5e6..097f1fb38 100644 --- a/client/packages/lowcoder/src/components/layout/SubSideBar.tsx +++ b/client/packages/lowcoder/src/components/layout/SubSideBar.tsx @@ -2,8 +2,8 @@ import { PropsWithChildren } from "react"; import styled from "styled-components"; const Wrapper = styled.div` - min-width: 232px; - width: 232px; + min-width: 280px; + width: 280px; height: 100%; background: #ffffff; border-right: 1px solid #f0f0f0; @@ -17,6 +17,7 @@ const Wrapper = styled.div` color: #222222; margin: 0 0 20px 20px; } + .ant-menu-inline .ant-menu-item { margin: 4px 0; padding: 10px 20px !important; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 71eaa7df7..611407fbd 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1581,7 +1581,7 @@ export const en = { "advanced": "Advanced", "lab": "Lab", "branding": "Branding", - "oauthProviders": "OAuth Providers", + "oauthProviders": "User Authentication", "appUsage": "App Usage Logs", "environments": "Environments", "premium": "Premium" @@ -2621,7 +2621,7 @@ export const en = { "table": table, }, "idSource": { - "title": "OAuth Providers", + "title": "User Authentication Provider", "form": "Email", "pay": "Premium", "enable": "Enable", diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 087503c6f..39c144e5d 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -1496,7 +1496,7 @@ settings: { advanced: "高级", lab: "实验室", branding: "品牌", - oauthProviders: "OAuth 提供商", + oauthProviders: "User Authentication", appUsage: "应用程序使用日志", environments: "环境", premium: "高级版", @@ -2560,7 +2560,7 @@ componentDocExtra: { table: table, }, idSource: { - title: "OAuth 提供商", + title: "用户认证提供商", form: "电子邮件", pay: "高级", enable: "启用", diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx index 76ccf765d..db14b3b01 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx @@ -5,7 +5,6 @@ import { FOLDER_URL_PREFIX, FOLDERS_URL, MARKETPLACE_URL, - MARKETPLACE_URL_BY_TYPE, MODULE_APPLICATIONS_URL, QUERY_LIBRARY_URL, SETTING, @@ -17,25 +16,17 @@ import { EditPopover, EllipsisTextCss, FolderIcon, - HomeActiveIcon, - HomeDataSourceActiveIcon, HomeDataSourceIcon, HomeIcon, - HomeModuleActiveIcon, HomeModuleIcon, - HomeQueryLibraryActiveIcon, HomeQueryLibraryIcon, - HomeSettingsActiveIcon, - HomeSettingsIcon, + HomeSettingIcon, InviteUserIcon, PlusIcon, PointIcon, - RecyclerActiveIcon, RecyclerIcon, MarketplaceIcon, - MarketplaceActiveIcon, - LowcoderMarketplaceActiveIcon, - LowcoderMarketplaceIcon, + AppsIcon } from "lowcoder-design"; import React, { useEffect, useState } from "react"; import { fetchAllApplications, fetchHomeData } from "redux/reduxActions/applicationActions"; @@ -345,48 +336,28 @@ export default function ApplicationHome() { text: {trans("home.allApplications")}, routePath: ALL_APPLICATIONS_URL, routeComp: HomeView, - icon: ({ selected, ...otherProps }) => - selected ? : , + icon: ({ selected, ...otherProps }) => selected ? : , }, { text: {trans("home.allModules")}, routePath: MODULE_APPLICATIONS_URL, routeComp: ModuleView, - icon: ({ selected, ...otherProps }) => - selected ? ( - - ) : ( - - ), + icon: ({ selected, ...otherProps }) => selected ? : , visible: ({ user }) => user.orgDev, }, { - text: ( - - {trans("home.marketplace")} - - ), + text: {trans("home.marketplace")}, routePath: MARKETPLACE_URL, routePathExact: false, routeComp: MarketplaceView, - icon: ({ selected, ...otherProps }) => - selected ? ( - - ) : ( - - ), + icon: ({ selected, ...otherProps }) => selected ? : , visible: ({ user }) => user.orgDev, }, { text: {trans("home.trash")}, routePath: TRASH_URL, routeComp: TrashView, - icon: ({ selected, ...otherProps }) => - selected ? ( - - ) : ( - - ), + icon: ({ selected, ...otherProps }) => selected ? : , visible: ({ user }) => user.orgDev, }, ], @@ -414,12 +385,7 @@ export default function ApplicationHome() { text: {trans("home.queryLibrary")}, routePath: QUERY_LIBRARY_URL, routeComp: QueryLibraryEditor, - icon: ({ selected, ...otherProps }) => - selected ? ( - - ) : ( - - ), + icon: ({ selected, ...otherProps }) => selected ? : , visible: ({ user }) => user.orgDev, }, { @@ -427,12 +393,7 @@ export default function ApplicationHome() { routePath: DATASOURCE_URL, routePathExact: false, routeComp: DatasourceHome, - icon: ({ selected, ...otherProps }) => - selected ? ( - - ) : ( - - ), + icon: ({ selected, ...otherProps }) => selected ? : , visible: ({ user }) => user.orgDev, onSelected: (_, currentPath) => currentPath.split("/")[1] === "datasource", }, @@ -441,12 +402,7 @@ export default function ApplicationHome() { routePath: SETTING, routePathExact: false, routeComp: Setting, - icon: ({ selected, ...otherProps }) => - selected ? ( - - ) : ( - - ), + icon: ({ selected, ...otherProps }) => selected ? : , visible: ({ user }) => user.orgDev, onSelected: (_, currentPath) => currentPath.split("/")[1] === "setting", }, diff --git a/client/packages/lowcoder/src/pages/setting/settingHome.tsx b/client/packages/lowcoder/src/pages/setting/settingHome.tsx index f54815278..388369a43 100644 --- a/client/packages/lowcoder/src/pages/setting/settingHome.tsx +++ b/client/packages/lowcoder/src/pages/setting/settingHome.tsx @@ -8,7 +8,14 @@ import AuditSetting from "@lowcoder-ee/pages/setting/audit"; import { isEE, isEnterpriseMode, isSelfDomain, showAuditLog } from "util/envUtils"; import { TwoColumnSettingPageContent } from "./styled"; import SubSideBar from "components/layout/SubSideBar"; -import { Menu } from "lowcoder-design"; +import { + Menu, + UserGroupIcon, + UserShieldIcon, + LeftSettingIcon, + ThemeIcon, + WorkspacesIcon + } from "lowcoder-design"; import { useSelector } from "react-redux"; import { getUser } from "redux/selectors/usersSelectors"; import history from "util/history"; @@ -37,25 +44,35 @@ export function SettingHome() { const selectKey = useParams<{ setting: string }>().setting || SettingPageEnum.UserGroups; const items = [ - { - key: SettingPageEnum.UserGroups, - label: trans("settings.userGroups"), - }, { key: SettingPageEnum.Organization, label: trans("settings.organization"), + icon: , + }, + { + key: SettingPageEnum.OAuthProvider, + label: (trans("settings.oauthProviders")), + disabled: !currentOrgAdmin(user), + icon: , + }, + { + key: SettingPageEnum.UserGroups, + label: trans("settings.userGroups"), + icon: , }, { key: SettingPageEnum.Theme, label: trans("settings.theme"), + icon: , }, { - key: SettingPageEnum.OAuthProvider, - label: ( - {trans("settings.oauthProviders")} - ), - disabled: !currentOrgAdmin(user), + key: SettingPageEnum.Advanced, + label: trans("settings.advanced"), + icon: , }, + + // Premium features + { key: SettingPageEnum.Environments, label: ( @@ -107,16 +124,13 @@ export function SettingHome() { !enableCustomBrand(config) || (!isSelfDomain(config) && !isEnterpriseMode(config)), }, - { - key: SettingPageEnum.Advanced, - label: trans("settings.advanced"), - }, ]; return ( - { From 1c44f3a6ed72b4e15cc015b783f62fd4fbe58ad1 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Fri, 1 Mar 2024 22:18:50 +0100 Subject: [PATCH 25/32] Changing Readme for latest ENV Variables --- deploy/docker/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/deploy/docker/README.md b/deploy/docker/README.md index ff70a597a..088d6653c 100644 --- a/deploy/docker/README.md +++ b/deploy/docker/README.md @@ -21,10 +21,11 @@ DOCKER_BUILDKIT=1 docker build -f deploy/docker/Dockerfile -t lowcoderorg/lowcod Image can be configured by setting environment variables. -| Environment variable | Description | Value | +| Environment variable | Description | Default-Value | |-------------------------------------| ----------------------------------------------------------------------- | ----------------------------------------------------- | | `LOWCODER_REDIS_ENABLED` | If **true** redis server is started in the container | `true` | | `LOWCODER_MONGODB_ENABLED` | If **true** mongo database is started in the container | `true` | +| `LOWCODER_MONGODB_EXPOSED` | If **true** mongo database accept connections from outside the docker | `false` | | `LOWCODER_API_SERVICE_ENABLED` | If **true** lowcoder api-service is started in the container | `true` | | `LOWCODER_NODE_SERVICE_ENABLED` | If **true** lowcoder node-service is started in the container | `true` | | `LOWCODER_FRONTEND_ENABLED` | If **true** lowcoder web frontend is started in the container | `true` | @@ -50,7 +51,12 @@ Image can be configured by setting environment variables. | `LOWCODER_CREATE_WORKSPACE_ON_SIGNUP` | IF LOWCODER_WORKSPACE_MODE = SAAS, controls if a own workspace is created for the user after sign up | `true` | | `LOWCODER_MARKETPLACE_PRIVATE_MODE` | Control if not to show Apps on the local Marketplace to anonymous users | `true` | +Also you should set the API-KEY secret, whcih should be a string of at least 32 random characters +On linux/mac, generate one eg. with: head /dev/urandom | head -c 30 | shasum -a 256 +| Environment variable | Description | Default-Value | +|-------------------------------------| ----------------------------------------------------------------------- | ----------------------------------------------------- | +| `LOWCODER_API_KEY_SECRET` | String to encrypt/sign API Keys that users may create | | ## Building api-service image @@ -69,7 +75,7 @@ DOCKER_BUILDKIT=1 docker build -f deploy/docker/Dockerfile -t lowcoderorg/lowcod Image can be configured by setting environment variables. -| Environment variable | Description | Value | +| Environment variable | Description | Default-Value | | --------------------------------| --------------------------------------------------------------------| ------------------------------------------------------| | `LOWCODER_PUID` | ID of user running services. It will own all created logs and data. | `9001` | | `LOWCODER_PGID` | ID of group of the user running services. | `9001` | @@ -105,7 +111,7 @@ DOCKER_BUILDKIT=1 docker build -f deploy/docker/Dockerfile -t lowcoderorg/lowcod Image can be configured by setting environment variables. -| Environment variable | Description | Value | +| Environment variable | Description | Default-Value | | --------------------------------| --------------------------------------------------------------------| ------------------------------------------------------- | | `LOWCODER_PUID` | ID of user running services. It will own all created logs and data. | `9001` | | `LOWCODER_PGID` | ID of group of the user running services. | `9001` | @@ -127,7 +133,7 @@ DOCKER_BUILDKIT=1 docker build -f deploy/docker/Dockerfile -t lowcoderorg/lowcod Image can be configured by setting environment variables. -| Environment variable | Description | Value | +| Environment variable | Description | Default-Value | | --------------------------------| --------------------------------------------------------------------| ------------------------------------------------------- | | `LOWCODER_PUID` | ID of user running services. It will own all created logs and data. | `9001` | | `LOWCODER_PGID` | ID of group of the user running services. | `9001` | From c96bb7e07b5d6b4672e7f7c7267d031d0cff5ee5 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Sun, 3 Mar 2024 22:47:36 +0100 Subject: [PATCH 26/32] Rounding Up Marketplace and Multi-Icon Component --- .../src/comps/comps/appSettingsComp.tsx | 2 +- .../lowcoder/src/comps/comps/iconComp.tsx | 1 + .../src/comps/comps/multiIconDisplay.tsx | 88 ++++++++ .../comps/tableComp/tablePropertyView.tsx | 2 +- .../src/constants/applicationConstants.ts | 2 +- .../packages/lowcoder/src/i18n/locales/en.ts | 1 + .../packages/lowcoder/src/i18n/locales/zh.ts | 1 + .../src/pages/ApplicationV2/HomeCardView.tsx | 6 +- .../src/pages/ApplicationV2/HomeLayout.tsx | 76 +++++-- .../src/pages/ApplicationV2/HomeResCard.tsx | 2 +- .../ApplicationV2/MarketplaceResCard.tsx | 194 ++++++++++++++++++ .../pages/ApplicationV2/MarketplaceView.tsx | 13 +- .../src/pages/editor/LeftLayersContent.tsx | 4 +- 13 files changed, 368 insertions(+), 24 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/multiIconDisplay.tsx create mode 100644 client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceResCard.tsx diff --git a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx index 54c294ea0..1782120df 100644 --- a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx @@ -172,7 +172,7 @@ const AppCategories = Object.keys(ApplicationCategoriesEnum).map( const value = ApplicationCategoriesEnum[cat as AppCategoriesEnumKey]; return { label: value, - value, + value: cat } } ) diff --git a/client/packages/lowcoder/src/comps/comps/iconComp.tsx b/client/packages/lowcoder/src/comps/comps/iconComp.tsx index d2db61596..c04c5bbb3 100644 --- a/client/packages/lowcoder/src/comps/comps/iconComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/iconComp.tsx @@ -144,3 +144,4 @@ IconBasicComp = class extends IconBasicComp { export const IconComp = withExposingConfigs(IconBasicComp, [ NameConfigHidden, ]); + diff --git a/client/packages/lowcoder/src/comps/comps/multiIconDisplay.tsx b/client/packages/lowcoder/src/comps/comps/multiIconDisplay.tsx new file mode 100644 index 000000000..cccb2a1fc --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/multiIconDisplay.tsx @@ -0,0 +1,88 @@ + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { findIconDefinition, library } from '@fortawesome/fontawesome-svg-core'; +import { fas } from '@fortawesome/free-solid-svg-icons'; +import { far } from '@fortawesome/free-regular-svg-icons'; +import * as AntdIcons from '@ant-design/icons'; + +library.add(far,fas); + +function parseIconIdentifier(identifier: string) { + if (identifier.startsWith('/icon:antd/')) { + let name = identifier.split('/')[2]; + return { type: 'antd', name }; + } + else if (identifier.startsWith('/icon:solid/') || identifier.startsWith('/icon:regular/')) { + const [style, name] = identifier.substring(6).split('/'); + return { type: 'fontAwesome', style, name }; + } + else if (identifier.startsWith('data:image')) { + return { type: 'base64', data: identifier, name: "" }; + } + else if (identifier.startsWith('http')) { + return { type: 'url', url: identifier, name: "" }; + } + else { + return { type: 'unknown', name: "" }; + } +} + +interface IconProps { + identifier: string; + width?: string; + height?: string; + style?: React.CSSProperties; +} + +const convertToCamelCase = (name: string) => { + return name.replace(/(-\w)/g, (match) => match[1].toUpperCase()); +} + +const appendStyleSuffix = (name: string) => { + if (name.endsWith('outlined')) { + return name.replace('outlined', 'Outlined'); + } else if (name.endsWith('filled')) { + return name.replace('filled', 'Filled'); + } else if (name.endsWith('twotone')) { + return name.replace('twotone', 'TwoTone'); + } + return name; +} + +// Multi icon Display Component + +const baseMultiIconDisplay: React.FC = ({ identifier, width = '24px', height = '24px', style }) => { + + const iconData = parseIconIdentifier(identifier); + + if (iconData.type === 'fontAwesome') { + const prefix = iconData.style === 'solid' ? 'fas' : 'far'; // 'fas' for solid, 'far' for regular + // Find the icon definition using prefix and iconName + const iconLookup = findIconDefinition({ prefix: prefix as any, iconName: iconData.name as any }); + + if (!iconLookup) { + console.error(`Icon ${iconData.name} with prefix ${prefix} not found`); + return null; + } + return ; + } + else if (iconData.type === 'antd') { + let iconName = convertToCamelCase(iconData.name); + iconName = appendStyleSuffix(iconName); + iconName = iconName.charAt(0).toUpperCase() + iconName.slice(1); + const AntdIcon = (AntdIcons as any)[iconName]; + if (!AntdIcon) { + console.error(`ANTd Icon ${iconData.name} not found`); + return null; + } + return ; + } + else if (iconData.type === 'url' || iconData.type === 'base64') { + return icon; + } + else { + return null; // Unknown type + } +}; + +export const MultiIconDisplay = baseMultiIconDisplay; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx index 58f8145c0..f45edf49b 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx @@ -272,7 +272,7 @@ function ColumnPropertyView>(props: { { - console.log("comp", comp); + // console.log("comp", comp); comp.dispatch( wrapChildAction( "columns", diff --git a/client/packages/lowcoder/src/constants/applicationConstants.ts b/client/packages/lowcoder/src/constants/applicationConstants.ts index e947676cc..895d78153 100644 --- a/client/packages/lowcoder/src/constants/applicationConstants.ts +++ b/client/packages/lowcoder/src/constants/applicationConstants.ts @@ -79,7 +79,7 @@ export interface ApplicationMeta { creatorEmail?: string; title?: string; description?: string; - icon?: string; + image?: string; category?: ApplicationCategoriesEnum; showheader?: boolean; orgId: string; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 611407fbd..19f6daf67 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -2266,6 +2266,7 @@ export const en = { "module": "Module", "trash": "Trash", "marketplace": "Marketplace", + "allCategories": "All Categories", "queryLibrary": "Query Library", "datasource": "Data Sources", "selectDatasourceType": "Select Data Source Type", diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 39c144e5d..d2e868a61 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -2210,6 +2210,7 @@ home: { "errorMarketplaceApps": "获取市场应用程序错误", "localMarketplaceTitle": "本地市场", "globalMarketplaceTitle": "Lowcoder 市场", + "allCategories": "所有类别", memberPermissionList: "成员权限:", orgName: "{orgName}管理员", addMember: "添加成员", diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeCardView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeCardView.tsx index e9f73cce8..ac515b574 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeCardView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeCardView.tsx @@ -1,14 +1,16 @@ import styled from "styled-components"; import { HomeRes } from "./HomeLayout"; import { HomeResCard } from "./HomeResCard"; +import { MarketplaceResCard } from "./MarketplaceResCard"; import React, { useState } from "react"; import { MoveToFolderModal } from "./MoveToFolderModal"; const ApplicationCardsWrapper = styled.div` display: grid; grid-template-columns: repeat(auto-fill, minmax(408px, 1fr)); - grid-template-rows: repeat(auto-fill, min(68px, 100%)); + grid-template-rows: repeat(auto-fill, min(auto, 100%)); grid-column-gap: 112px; + grid-row-gap: 20px; margin: 48px 26px 80px; overflow: hidden; @media screen and (max-width: 500px) { @@ -23,6 +25,8 @@ export function HomeCardView(props: { resources: HomeRes[] }) { return ( {props.resources.map((res) => ( + res.isMarketplace ? + : ))} setNeedMoveRes(undefined)} /> diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx index 1442e1620..5cf5d7e36 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx @@ -35,6 +35,7 @@ import { checkIsMobile } from "util/commonUtils"; import MarketplaceHeaderImage from "assets/images/marketplaceHeaderImage.jpg"; import { Divider } from "antd"; import { Margin } from "../setting/theme/styledComponents"; +import { ApplicationCategoriesEnum } from "constants/applicationConstants"; const Wrapper = styled.div` display: flex; @@ -171,7 +172,7 @@ const FilterDropdown = styled(Select)` const FilterMenuItem = styled.div` display: flex; - align-items: center; + align-items: left; height: 29px; width: 100%; `; @@ -253,6 +254,10 @@ export interface HomeRes { key: string; id: string; name: string; + title?: string; + description?: string; + category?: string; + icon?: string; type: HomeResTypeEnum; creator: string; lastModifyTime: number; @@ -276,20 +281,37 @@ export interface HomeLayoutProps { } export function HomeLayout(props: HomeLayoutProps) { + + const { breadcrumb = [], elements = [], localMarketplaceApps = [], globalMarketplaceApps = [],mode } = props; + + const categoryOptions = [ + { label: {trans("home.allCategories")}, value: 'All' }, + ...Object.entries(ApplicationCategoriesEnum).map(([key, value]) => ({ + label: ( + + {value} + + ), + value: key, + })), + ]; + const user = useSelector(getUser); const isFetching = useSelector(isFetchingFolderElements); const isSelfHost = window.location.host !== 'app.lowcoder.cloud'; - const [filterBy, setFilterBy] = useState("All"); + const [typeFilter, setTypeFilter] = useState("All"); + const [categoryFilter, setCategoryFilter] = useState("All"); const [searchValue, setSearchValue] = useState(""); const [layout, setLayout] = useState( checkIsMobile(window.innerWidth) ? "card" : getHomeLayout() ); + useEffect(() => saveHomeLayout(layout), [layout]); useEffect(() => { - // remove collision status from localstorage + // remove collision status from localstorage, as the next selected app may have another collision status removeCollisionStatus(); }, []); @@ -300,6 +322,7 @@ export function HomeLayout(props: HomeLayoutProps) { } var displayElements = elements; + if (mode === "marketplace" && isSelfHost) { const markedLocalApps = localMarketplaceApps.map(app => ({ ...app, isLocalMarketplace: true })); const markedGlobalApps = globalMarketplaceApps.map(app => ({ ...app, isLocalMarketplace: false })); @@ -319,18 +342,27 @@ export function HomeLayout(props: HomeLayoutProps) { : true ) .filter((e) => { - if (HomeResTypeEnum[filterBy].valueOf() === HomeResTypeEnum.All) { + if (HomeResTypeEnum[typeFilter].valueOf() === HomeResTypeEnum.All) { return true; } if (e.folder) { - return HomeResTypeEnum[filterBy] === HomeResTypeEnum.Folder; + return HomeResTypeEnum[typeFilter] === HomeResTypeEnum.Folder; } else { - if (filterBy === "Navigation") { + if (typeFilter === "Navigation") { return NavigationTypes.map((t) => t.valueOf()).includes(e.applicationType); } - return HomeResTypeEnum[filterBy].valueOf() === e.applicationType; + return HomeResTypeEnum[typeFilter].valueOf() === e.applicationType; + } + }) + .filter((e) => { + // If "All" is selected, do not filter out any elements based on category + if (categoryFilter === 'All' || !categoryFilter) { + return true; } + // Otherwise, filter elements based on the selected category + return !e.folder && e.category === categoryFilter.toString(); }) + .map((e) => e.folder ? { @@ -347,6 +379,10 @@ export function HomeLayout(props: HomeLayoutProps) { key: e.applicationId, id: e.applicationId, name: e.name, + title: e.title, + description: e.description, + category: e.category, + icon: e.image, type: HomeResTypeEnum[HomeResTypeEnum[e.applicationType] as HomeResKey], creator: e?.creatorEmail ?? e.createBy, lastModifyTime: e.lastModifyTime, @@ -385,6 +421,14 @@ export function HomeLayout(props: HomeLayoutProps) { })) ] + const testOptions = [ + getFilterMenuItem(HomeResTypeEnum.All), + getFilterMenuItem(HomeResTypeEnum.Application), + getFilterMenuItem(HomeResTypeEnum.Module), + ...(mode !== "marketplace" ? [getFilterMenuItem(HomeResTypeEnum.Navigation)] : []), + ...(mode !== "trash" && mode !== "marketplace" ? [getFilterMenuItem(HomeResTypeEnum.Folder)] : []), + ]; + return ( @@ -414,19 +458,27 @@ export function HomeLayout(props: HomeLayoutProps) { {mode !== "folders" && mode !== "module" && ( setFilterBy(value as HomeResKey)} + value={typeFilter} + onChange={(value: any) => setTypeFilter(value as HomeResKey)} options={[ getFilterMenuItem(HomeResTypeEnum.All), getFilterMenuItem(HomeResTypeEnum.Application), getFilterMenuItem(HomeResTypeEnum.Module), ...(mode !== "marketplace" ? [getFilterMenuItem(HomeResTypeEnum.Navigation)] : []), ...(mode !== "trash" && mode !== "marketplace" ? [getFilterMenuItem(HomeResTypeEnum.Folder)] : []), - ]} getPopupContainer={(node: any) => node} - suffixIcon={} - /> + suffixIcon={} /> + )} + {mode === "marketplace" && ( + setCategoryFilter(value as ApplicationCategoriesEnum)} + options={categoryOptions} + // getPopupContainer={(node) => node} + suffixIcon={} /> )} diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeResCard.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeResCard.tsx index ceef8b54a..4e6069f05 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeResCard.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeResCard.tsx @@ -73,7 +73,7 @@ const Card = styled.div` align-items: center; height: 100%; width: 100%; - border-bottom: 1px solid #f5f5f6; + padding: 0 10px; button { diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceResCard.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceResCard.tsx new file mode 100644 index 000000000..80f4d5667 --- /dev/null +++ b/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceResCard.tsx @@ -0,0 +1,194 @@ +import { TacoButton } from "lowcoder-design"; +import styled from "styled-components"; +import { timestampToHumanReadable } from "util/dateTimeUtils"; +import { HomeRes } from "./HomeLayout"; +import { + handleMarketplaceAppViewClick, + HomeResInfo, +} from "../../util/homeResUtils"; +import { trans } from "../../i18n"; +import { checkIsMobile } from "util/commonUtils"; +import history from "util/history"; +import { APPLICATION_VIEW_URL } from "constants/routesURL"; +import { TypographyText } from "../../components/TypographyText"; +import { messageInstance } from "lowcoder-design"; +import { Typography } from "antd"; +import { MultiIconDisplay } from "../../comps/comps/multiIconDisplay"; + +const { Text } = Typography; + +const EditButton = styled(TacoButton)` + width: 52px; + height: 24px; + padding: 5px 12px; + margin-right: 12px; + @media screen and (max-width: 500px) { + display: none; + } +`; + +const ExecButton = styled(TacoButton)` + width: 52px; + height: 24px; + padding: 5px 12px; + margin-right: 24px; + background: #fafbff; + border: 1px solid #c9d1fc; + border-radius: 4px; + font-weight: 500; + color: #4965f2; + + &:hover { + background: #f9fbff; + border: 1px solid #c2d6ff; + color: #315efb; + } + + @media screen and (max-width: 500px) { + margin-right: 0; + display: none; + } +`; + +const Wrapper = styled.div` + height: auto; + padding: 0 6px; + border-radius: 8px; + margin-bottom: -1px; + margin-top: 1px; + background-color: #fcfcfc; + + &:hover { + background-color: #f5f7fa; + } +`; + +const Card = styled.div` + display: flex; + align-items: center; + height: 100%; + width: 100%; + border-bottom: 1px solid #f5f5f6; + padding: 0 10px; + + button { + opacity: 0; + } + + &:hover { + button { + opacity: 1; + } + } + + @media screen and (max-width: 500px) { + button { + opacity: 1; + } + + padding: 0; + } +`; + +const CardInfo = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 14px; + white-space: wrap; + width: 284px; + max-height: 150px; + flex-grow: 1; + cursor: pointer; + overflow: hidden; + padding-right: 12px; + padding-top: 12px; + + &:hover { + .ant-typography { + color: #315efb; + } + } + + .ant-typography { + padding: 2px 2px 8px 2px; + } +`; + +const AppTimeOwnerInfoLabel = styled.div` + font-size: 13px; + color: #8b8fa3; + line-height: 15px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +const AppDescription = styled.div` + font-size: 13px; + color: #8b8fa3; + line-height: 15px; + overflow: hidden; + white-space: wrap; + text-overflow: ellipsis; + margin-top: 10px; + margin-bottom: 10px; +`; + +const OperationWrapper = styled.div` + display: flex; + align-items: center; + @media screen and (max-width: 500px) { + > svg { + display: none; + } + } +`; + +const MONTH_MILLIS = 30 * 24 * 60 * 60 * 1000; + +export function MarketplaceResCard(props: { res: HomeRes; }) { + const { res } = props; + + const subTitle = trans("home.resCardSubTitle", { time: timestampToHumanReadable(res.lastModifyTime, MONTH_MILLIS), creator: res.creator}); + + const resInfo = HomeResInfo[res.type]; + if (!resInfo) { return null; } + + return ( + + + {res.icon && typeof res.icon === 'string' && ( + + )} + { + if (checkIsMobile(window.innerWidth)) { + history.push(APPLICATION_VIEW_URL(res.id, "view")); + return; + } + if(res.isMarketplace) { + handleMarketplaceAppViewClick(res.id); + return; + } + }} + > + {}} + /> + {subTitle} + {res.description && + {res.description.length > 150 ? res.description.substring(0, 150) + '...' : res.description} + } + + + handleMarketplaceAppViewClick(res.id)}> + {trans("view")} + + + + + ); +} diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceView.tsx index 55df189c3..10e5b5090 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceView.tsx @@ -1,20 +1,20 @@ import { useEffect, useState } from "react"; import { HomeLayout } from "./HomeLayout"; -import { MARKETPLACE_TYPE_URL, MARKETPLACE_URL } from "constants/routesURL"; +import { MARKETPLACE_URL } from "constants/routesURL"; import { trans } from "../../i18n"; import axios, { AxiosResponse } from "axios"; import ApplicationApi from "@lowcoder-ee/api/applicationApi"; -import { ApplicationMeta, MarketplaceType } from "@lowcoder-ee/constants/applicationConstants"; +import { ApplicationMeta } from "@lowcoder-ee/constants/applicationConstants"; import { GenericApiResponse } from "@lowcoder-ee/api/apiResponses"; import { validateResponse } from "@lowcoder-ee/api/apiUtils"; import { messageInstance } from "lowcoder-design"; -import { matchPath } from "react-router"; -import log from "loglevel"; export function MarketplaceView() { const [ marketplaceApps, setMarketplaceApps ] = useState>([]); const [ localMarketplaceApps, setLocalMarketplaceApps ] = useState>([]); + // console.log("localMarketplaceApps", localMarketplaceApps); + const fetchMarketplaceApps = async () => { try { let response: AxiosResponse>; @@ -46,8 +46,11 @@ export function MarketplaceView() { } useEffect(() => { + // Make sure we are fetching local marketplace apps for self-hosted environments + if (window.location.host !== 'app.lowcoder.cloud') { + fetchLocalMarketplaceApps(); + } fetchMarketplaceApps(); - fetchLocalMarketplaceApps(); }, []); return ( diff --git a/client/packages/lowcoder/src/pages/editor/LeftLayersContent.tsx b/client/packages/lowcoder/src/pages/editor/LeftLayersContent.tsx index f2d6c6122..178e25138 100644 --- a/client/packages/lowcoder/src/pages/editor/LeftLayersContent.tsx +++ b/client/packages/lowcoder/src/pages/editor/LeftLayersContent.tsx @@ -256,7 +256,7 @@ export const LeftLayersContent = (props: LeftLayersContentProps) => { children[types[0]]?.dispatchChangeValueAction(color); } else if(types.length === 2) { // nested object e.g. style.background - console.log(children[types[0]]); + // (children[types[0]]); if (!children[types[0]]) { if (children[compType].children[types[0]]?.children[types[1]]) { children[compType].children[types[0]].children[types[1]]?.dispatchChangeValueAction(color); @@ -332,7 +332,7 @@ export const LeftLayersContent = (props: LeftLayersContentProps) => { children[types[0]]?.dispatchChangeValueAction(value); } else if(types.length === 2) { // nested object e.g. style.background - console.log(children[types[0]]); + // console.log(children[types[0]]); if (!children[types[0]]) { if (children[compType].children[types[0]]?.children[types[1]]) { children[compType].children[types[0]].children[types[1]]?.dispatchChangeValueAction(value); From cc5333c193a5c1d96bd45d76c384771d52b6c7d8 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Thu, 29 Feb 2024 11:13:00 +0100 Subject: [PATCH 27/32] Added comments based on common discussion --- .../repository/ApplicationRepository.java | 13 +++-- .../service/ApplicationService.java | 54 +++++++++++++++++++ .../application/ApplicationController.java | 3 ++ 3 files changed, 67 insertions(+), 3 deletions(-) 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 c6b2951fd..a765e9103 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 @@ -18,6 +18,7 @@ @Repository public interface ApplicationRepository extends ReactiveMongoRepository, CustomApplicationRepository { + // publishedApplicationDSL : 0 -> excludes publishedApplicationDSL from the return @Query(fields = "{ publishedApplicationDSL : 0 , editingApplicationDSL : 0 }") Flux findByOrganizationId(String organizationId); @@ -37,14 +38,20 @@ public interface ApplicationRepository extends ReactiveMongoRepository findByPublicToAllIsTrueAndPublicToMarketplaceIsOrAgencyProfileIsAndIdIn - (Boolean publicToMarketplace, Boolean agencyProfile, Collection ids); + (Boolean publicToMarketplace, Boolean agencyProfile, Collection ids); */ - Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsAndAgencyProfileIsAndIdIn(Boolean publicToMarketplace, Boolean agencyProfile, Collection ids); + // this we do not need + // Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsAndAgencyProfileIsAndIdIn(Boolean publicToMarketplace, Boolean agencyProfile, Collection ids); + // Find all Public Applications + Flux findByPublicToAllIsTrue(); + + // Find all Marketplace Apps Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsTrue(); + // Find all Agencies Flux findByPublicToAllIsTrueAndAgencyProfileIsTrue(); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java index 34b24f1e2..e8abe8242 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java @@ -204,10 +204,64 @@ public Mono setApplicationAsAgencyProfile(String applicationId, boolean return mongoUpsertHelper.updateById(application, applicationId); } + // getPublicApplicationIds /view - publicToAll check + // getPublicMarketplaceApplicationIds / marketplace_view - publicToAll and publicToMarketplace check & isPrivateMarketplace check + // getPublicAgencyProfileApplicationIds / agency_profile_view - publicToAll and agencyProfile check + + // marketplace_view [anonymous] publicToAll and publicToMarketplace check & isPrivateMarketplace false -> OK + + // marketplace_view [anonymous] publicToAll and publicToMarketplace check & isPrivateMarketplace true -> NOT OK + + // marketplace_view [LoggedIn] publicToAll and publicToMarketplace check & isPrivateMarketplace true -> OK + // marketplace_view [LoggedIn] publicToAll and publicToMarketplace check & isPrivateMarketplace false -> OK + + + // will be extended by EndpointType + /* + * if (EndpointType == view) + * if (EndpointType == marketplace_view) + * if (EndpointType == agency_profile_view) + */ + + // is it needed? @NonEmptyMono @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") public Mono> getPublicApplicationIds(Collection applicationIds, Boolean isAnonymous, Boolean isPrivateMarketplace) { + return repository.findByPublicToAllIsTrue() + .map(HasIdAndAuditing::getId) + .collect(Collectors.toSet()); + } + + // for Marketplaces + @NonEmptyMono + @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") + public Mono> getPublicMarketplaceApplicationIds(Collection applicationIds, Boolean isAnonymous, Boolean isPrivateMarketplace) { + + if(isAnonymous) { + if(isPrivateMarketplace) { + return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsTrue(false, false, applicationIds) + .map(HasIdAndAuditing::getId) + .collect(Collectors.toSet()); + } else { + return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsTrue(true, false, applicationIds) + .map(HasIdAndAuditing::getId) + .collect(Collectors.toSet()); + } + } else { + return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsTrue(true, true, applicationIds) + .map(HasIdAndAuditing::getId) + .collect(Collectors.toSet()); + } + + + } + + // for Agencies + @NonEmptyMono + @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") + public Mono> getPublicAgencyApplicationIds(Collection applicationIds, Boolean isAnonymous, Boolean isPrivateMarketplace) { + if(isAnonymous) { if(isPrivateMarketplace) { return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsAndAgencyProfileIsAndIdIn(false, false, applicationIds) 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 86be1e576..5270b9a19 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 @@ -16,6 +16,8 @@ import org.lowcoder.api.application.view.ApplicationPermissionView; import org.lowcoder.api.application.view.ApplicationView; import org.lowcoder.api.application.view.MarketplaceApplicationInfoView; +// should we not have a AgencyApplicationInfoView + import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.api.home.SessionUserService; import org.lowcoder.api.home.UserHomeApiService; @@ -91,6 +93,7 @@ public Mono> getEditingApplication(@PathVariable S .map(ResponseView::success); } + // will call the check in ApplicationApiService and ApplicationService @Override public Mono> getPublishedApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_ALL) From 120fbc929a6a09e2e88f076c45745929ce4d7ac4 Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Thu, 29 Feb 2024 20:39:33 +0100 Subject: [PATCH 28/32] fix: sorted out application view rights for all cases --- .../model/ApplicationRequestType.java | 7 ++ .../repository/ApplicationRepository.java | 29 +++-- .../service/ApplicationService.java | 102 +++++++--------- .../service/ApplicationPermissionHandler.java | 39 +++++- .../service/DatasourcePermissionHandler.java | 13 ++ .../service/ResourcePermissionHandler.java | 67 +++++++++++ .../service/ResourcePermissionService.java | 9 ++ .../application/ApplicationApiService.java | 111 +++++++++--------- .../application/ApplicationController.java | 10 +- .../api/application/ApplicationEndpoints.java | 6 - .../ApplicationApiServiceTest.java | 2 +- 11 files changed, 258 insertions(+), 137 deletions(-) create mode 100644 server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationRequestType.java diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationRequestType.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationRequestType.java new file mode 100644 index 000000000..ca5e63ea1 --- /dev/null +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationRequestType.java @@ -0,0 +1,7 @@ +package org.lowcoder.domain.application.model; + +public enum ApplicationRequestType { + PUBLIC_TO_ALL, + PUBLIC_TO_MARKETPLACE, + AGENCY_PROFILE, +} 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 a765e9103..c75402472 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 @@ -45,13 +45,28 @@ public interface ApplicationRepository extends ReactiveMongoRepository findByPublicToAllIsTrueAndPublicToMarketplaceIsAndAgencyProfileIsAndIdIn(Boolean publicToMarketplace, Boolean agencyProfile, Collection ids); - // Find all Public Applications - Flux findByPublicToAllIsTrue(); - - // Find all Marketplace Apps + /** + * Filter public applications from list of supplied IDs + */ + Flux findByPublicToAllIsTrueAndIdIn(Collection ids); + + /** + * Filter marketplace applications from list of supplied IDs + */ + Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsTrueAndIdIn(Collection ids); + + /** + * Filter agency applications from list of supplied IDs + */ + Flux findByPublicToAllIsTrueAndAgencyProfileIsTrueAndIdIn(Collection ids); + + /** + * Find all marketplace applications + */ Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsTrue(); - - // Find all Agencies + + /** + * Find all agency applications + */ Flux findByPublicToAllIsTrueAndAgencyProfileIsTrue(); - } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java index e8abe8242..e6e9b9b39 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java @@ -7,6 +7,7 @@ import java.util.stream.Collectors; import org.lowcoder.domain.application.model.Application; +import org.lowcoder.domain.application.model.ApplicationRequestType; import org.lowcoder.domain.application.model.ApplicationStatus; import org.lowcoder.domain.application.repository.ApplicationRepository; import org.lowcoder.domain.permission.model.ResourceRole; @@ -157,8 +158,6 @@ public Mono setApplicationPublicToMarketplace(String applicationId, Boo return findById(applicationId) - // Falk: question - do we need Map applicationDsl = application.getEditingApplicationDSL(); and .editingApplicationDSL(applicationDsl) - or is .publicToMarketplace(publicToMarketplace).build(); enough? - .map(application -> { Map applicationDsl = application.getEditingApplicationDSL(); @@ -204,81 +203,64 @@ public Mono setApplicationAsAgencyProfile(String applicationId, boolean return mongoUpsertHelper.updateById(application, applicationId); } - // getPublicApplicationIds /view - publicToAll check - // getPublicMarketplaceApplicationIds / marketplace_view - publicToAll and publicToMarketplace check & isPrivateMarketplace check - // getPublicAgencyProfileApplicationIds / agency_profile_view - publicToAll and agencyProfile check - // marketplace_view [anonymous] publicToAll and publicToMarketplace check & isPrivateMarketplace false -> OK + @NonEmptyMono + @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") + public Mono> getFilteredPublicApplicationIds(ApplicationRequestType requestType, Collection applicationIds, Boolean isAnonymous, Boolean isPrivateMarketplace) { + + switch(requestType) + { + case PUBLIC_TO_ALL: + return getPublicApplicationIds(applicationIds); + case PUBLIC_TO_MARKETPLACE: + return getPublicMarketplaceApplicationIds(applicationIds, isAnonymous, isPrivateMarketplace); + case AGENCY_PROFILE: + return getPublicAgencyApplicationIds(applicationIds); + default: + return Mono.empty(); + } + } - // marketplace_view [anonymous] publicToAll and publicToMarketplace check & isPrivateMarketplace true -> NOT OK - - // marketplace_view [LoggedIn] publicToAll and publicToMarketplace check & isPrivateMarketplace true -> OK - // marketplace_view [LoggedIn] publicToAll and publicToMarketplace check & isPrivateMarketplace false -> OK - - - // will be extended by EndpointType - /* - * if (EndpointType == view) - * if (EndpointType == marketplace_view) - * if (EndpointType == agency_profile_view) + + /** + * Find all public applications - doesn't matter if user is anonymous, because these apps are public */ - - // is it needed? @NonEmptyMono @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") - public Mono> getPublicApplicationIds(Collection applicationIds, Boolean isAnonymous, Boolean isPrivateMarketplace) { + public Mono> getPublicApplicationIds(Collection applicationIds) { - return repository.findByPublicToAllIsTrue() + return repository.findByPublicToAllIsTrueAndIdIn(applicationIds) .map(HasIdAndAuditing::getId) .collect(Collectors.toSet()); } - // for Marketplaces + + /** + * Find all marketplace applications - filter based on whether user is anonymous and whether it's a private marketplace + */ @NonEmptyMono @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") - public Mono> getPublicMarketplaceApplicationIds(Collection applicationIds, Boolean isAnonymous, Boolean isPrivateMarketplace) { - - if(isAnonymous) { - if(isPrivateMarketplace) { - return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsTrue(false, false, applicationIds) - .map(HasIdAndAuditing::getId) - .collect(Collectors.toSet()); - } else { - return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsTrue(true, false, applicationIds) - .map(HasIdAndAuditing::getId) - .collect(Collectors.toSet()); - } - } else { - return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsTrue(true, true, applicationIds) - .map(HasIdAndAuditing::getId) - .collect(Collectors.toSet()); - } - - + public Mono> getPublicMarketplaceApplicationIds(Collection applicationIds, boolean isAnonymous, boolean isPrivateMarketplace) { + + if ((isAnonymous && !isPrivateMarketplace) || !isAnonymous) + { + return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsTrueAndIdIn(applicationIds) + .map(HasIdAndAuditing::getId) + .collect(Collectors.toSet()); + } + return Mono.empty(); } - // for Agencies + /** + * Find all agency applications + */ @NonEmptyMono @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") - public Mono> getPublicAgencyApplicationIds(Collection applicationIds, Boolean isAnonymous, Boolean isPrivateMarketplace) { - - if(isAnonymous) { - if(isPrivateMarketplace) { - return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsAndAgencyProfileIsAndIdIn(false, false, applicationIds) - .map(HasIdAndAuditing::getId) - .collect(Collectors.toSet()); - } else { - return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsAndAgencyProfileIsAndIdIn(true, false, applicationIds) - .map(HasIdAndAuditing::getId) - .collect(Collectors.toSet()); - } - } else { - return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsOrAgencyProfileIsAndIdIn(true, true, applicationIds) - .map(HasIdAndAuditing::getId) - .collect(Collectors.toSet()); - } - + public Mono> getPublicAgencyApplicationIds(Collection applicationIds) { + return repository.findByPublicToAllIsTrueAndAgencyProfileIsTrueAndIdIn(applicationIds) + .map(HasIdAndAuditing::getId) + .collect(Collectors.toSet()); } public Flux findAll() { diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ApplicationPermissionHandler.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ApplicationPermissionHandler.java index 5d6448d13..00bd9d987 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ApplicationPermissionHandler.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ApplicationPermissionHandler.java @@ -15,6 +15,7 @@ import java.util.Set; import org.lowcoder.domain.application.model.Application; +import org.lowcoder.domain.application.model.ApplicationRequestType; import org.lowcoder.domain.application.service.ApplicationService; import org.lowcoder.domain.permission.model.ResourceAction; import org.lowcoder.domain.permission.model.ResourcePermission; @@ -46,7 +47,7 @@ protected Mono>> getAnonymousUserPermission } Set applicationIds = newHashSet(resourceIds); - return Mono.zip(applicationService.getPublicApplicationIds(applicationIds, Boolean.TRUE, config.getMarketplace().isPrivateMode()), + return Mono.zip(applicationService.getPublicApplicationIds(applicationIds), templateSolution.getTemplateApplicationIds(applicationIds)) .map(tuple -> { Set publicAppIds = tuple.getT1(); @@ -61,7 +62,7 @@ protected Mono>> getAnonymousUserPermission (Collection resourceIds, ResourceAction resourceAction) { Set applicationIds = newHashSet(resourceIds); - return Mono.zip(applicationService.getPublicApplicationIds(applicationIds, Boolean.FALSE, config.getMarketplace().isPrivateMode()), + return Mono.zip(applicationService.getPublicApplicationIds(applicationIds), templateSolution.getTemplateApplicationIds(applicationIds)) .map(tuple -> { Set publicAppIds = tuple.getT1(); @@ -70,7 +71,39 @@ protected Mono>> getAnonymousUserPermission }); } - private List getAnonymousUserPermission(String applicationId) { + + @Override + protected Mono>> getAnonymousUserApplicationPermissions( + Collection resourceIds, ResourceAction resourceAction, ApplicationRequestType requestType) + { + if (!ANONYMOUS_USER_ROLE.canDo(resourceAction)) { + return Mono.just(emptyMap()); + } + + Set applicationIds = newHashSet(resourceIds); + return Mono.zip(applicationService.getFilteredPublicApplicationIds(requestType, applicationIds, Boolean.TRUE, config.getMarketplace().isPrivateMode()), + templateSolution.getTemplateApplicationIds(applicationIds)) + .map(tuple -> { + Set publicAppIds = tuple.getT1(); + Set templateAppIds = tuple.getT2(); + return collectMap(union(publicAppIds, templateAppIds), identity(), this::getAnonymousUserPermission); + }); + } + + @Override + protected Mono>> getNonAnonymousUserApplicationPublicResourcePermissions( + Collection resourceIds, ResourceAction resourceAction, ApplicationRequestType requestType) { + Set applicationIds = newHashSet(resourceIds); + return Mono.zip(applicationService.getFilteredPublicApplicationIds(requestType, applicationIds, Boolean.FALSE, config.getMarketplace().isPrivateMode()), + templateSolution.getTemplateApplicationIds(applicationIds)) + .map(tuple -> { + Set publicAppIds = tuple.getT1(); + Set templateAppIds = tuple.getT2(); + return collectMap(union(publicAppIds, templateAppIds), identity(), this::getAnonymousUserPermission); + }); + } + + private List getAnonymousUserPermission(String applicationId) { return Collections.singletonList(ResourcePermission.builder() .resourceId(applicationId) .resourceType(ResourceType.APPLICATION) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/DatasourcePermissionHandler.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/DatasourcePermissionHandler.java index 130ee8033..75e034218 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/DatasourcePermissionHandler.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/DatasourcePermissionHandler.java @@ -10,6 +10,7 @@ import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; +import org.lowcoder.domain.application.model.ApplicationRequestType; import org.lowcoder.domain.datasource.model.Datasource; import org.lowcoder.domain.datasource.service.DatasourceService; import org.lowcoder.domain.permission.model.ResourceAction; @@ -44,6 +45,18 @@ protected Mono>> getNonAnonymousUserPublicR } @Override + protected Mono>> getAnonymousUserApplicationPermissions( + Collection resourceIds, ResourceAction resourceAction, ApplicationRequestType requestType) { + return Mono.just(Collections.emptyMap()); + } + + @Override + protected Mono>> getNonAnonymousUserApplicationPublicResourcePermissions( + Collection resourceIds, ResourceAction resourceAction, ApplicationRequestType requestType) { + return Mono.just(Collections.emptyMap()); + } + + @Override protected Mono getOrgId(String resourceId) { return datasourceService.getById(resourceId) .map(Datasource::getOrganizationId); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java index 09efd31f2..8b0587480 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java @@ -18,6 +18,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; +import org.lowcoder.domain.application.model.ApplicationRequestType; import org.lowcoder.domain.group.service.GroupMemberService; import org.lowcoder.domain.organization.service.OrgMemberService; import org.lowcoder.domain.permission.model.ResourceAction; @@ -153,6 +154,13 @@ protected abstract Mono>> getAnonymousUserP protected abstract Mono>> getNonAnonymousUserPublicResourcePermissions (Collection resourceIds, ResourceAction resourceAction); + protected abstract Mono>> getAnonymousUserApplicationPermissions(Collection resourceIds, + ResourceAction resourceAction, ApplicationRequestType requestType); + + protected abstract Mono>> getNonAnonymousUserApplicationPublicResourcePermissions + (Collection resourceIds, ResourceAction resourceAction, ApplicationRequestType requestType); + + private Mono>> getAllMatchingPermissions0(String userId, String orgId, ResourceType resourceType, Collection resourceIds, ResourceAction resourceAction) { @@ -212,4 +220,63 @@ private Mono> getUserGroupIds(String orgId, String userId) { } protected abstract Mono getOrgId(String resourceId); + + public Mono checkUserPermissionStatusOnApplication(String userId, String resourceId, + ResourceAction resourceAction, ApplicationRequestType requestType) + { + ResourceType resourceType = resourceAction.getResourceType(); + + Mono publicResourcePermissionMono = getAnonymousUserApplicationPermissions(singletonList(resourceId), resourceAction, requestType) + .map(it -> it.getOrDefault(resourceId, emptyList())) + .map(it -> { + if (!it.isEmpty()) { + return UserPermissionOnResourceStatus.success(it.get(0)); + } + return isAnonymousUser(userId) ? UserPermissionOnResourceStatus.anonymousUser() : UserPermissionOnResourceStatus.notInOrg(); + }); + + if (isAnonymousUser(userId)) { + return publicResourcePermissionMono; + } + + Mono nonAnonymousPublicResourcePermissionMono = getNonAnonymousUserApplicationPublicResourcePermissions(singletonList(resourceId), resourceAction, requestType) + .map(it -> it.getOrDefault(resourceId, emptyList())) + .map(it -> { + if (!it.isEmpty()) { + return UserPermissionOnResourceStatus.success(it.get(0)); + } + return isAnonymousUser(userId) ? UserPermissionOnResourceStatus.anonymousUser() : UserPermissionOnResourceStatus.notInOrg(); + }); + + + Mono orgUserPermissionMono = getOrgId(resourceId) + .flatMap(orgId -> orgMemberService.getOrgMember(orgId, userId)) + .flatMap(orgMember -> { + if (orgMember.isAdmin()) { + return Mono.just(UserPermissionOnResourceStatus.success(buildAdminPermission(resourceType, resourceId, userId))); + } + return getAllMatchingPermissions0(userId, orgMember.getOrgId(), resourceType, Collections.singleton(resourceId), resourceAction) + .map(it -> it.getOrDefault(resourceId, emptyList())) + .map(permissions -> permissions.isEmpty() ? UserPermissionOnResourceStatus.notEnoughPermission() + : UserPermissionOnResourceStatus.success(getMaxPermission(permissions))); + }) + .defaultIfEmpty(UserPermissionOnResourceStatus.notInOrg()); + + return Mono.zip(publicResourcePermissionMono, nonAnonymousPublicResourcePermissionMono, orgUserPermissionMono) + .map(tuple -> { + UserPermissionOnResourceStatus publicResourcePermission = tuple.getT1(); + UserPermissionOnResourceStatus nonAnonymousPublicResourcePermission = tuple.getT2(); + UserPermissionOnResourceStatus orgUserPermission = tuple.getT3(); + if (orgUserPermission.hasPermission()) { + return orgUserPermission; + } + if(nonAnonymousPublicResourcePermission.hasPermission()) { + return nonAnonymousPublicResourcePermission; + } + if (publicResourcePermission.hasPermission()) { + return publicResourcePermission; + } + return orgUserPermission; + }); + } } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionService.java index 9cdba0e30..8dd06e9d4 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionService.java @@ -19,6 +19,7 @@ import javax.validation.constraints.NotNull; import org.apache.commons.collections4.CollectionUtils; +import org.lowcoder.domain.application.model.ApplicationRequestType; import org.lowcoder.domain.permission.model.ResourceAction; import org.lowcoder.domain.permission.model.ResourceHolder; import org.lowcoder.domain.permission.model.ResourcePermission; @@ -221,6 +222,14 @@ public Mono checkAndReturnMaxPermission(String userId, Strin return resourcePermissionHandler.checkUserPermissionStatusOnResource(userId, resourceId, resourceAction); } + public Mono checkUserPermissionStatusOnApplication + (String userId, String resourceId, ResourceAction resourceAction, ApplicationRequestType requestType) { + ResourceType resourceType = resourceAction.getResourceType(); + var resourcePermissionHandler = getResourcePermissionHandler(resourceType); + return resourcePermissionHandler.checkUserPermissionStatusOnApplication(userId, resourceId, resourceAction, requestType); +} + + public Mono removeUserApplicationPermission(String appId, String userId) { return repository.removePermissionBy(ResourceType.APPLICATION, appId, ResourceHolder.USER, userId); } 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 6ced87d03..c2a23b11a 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 @@ -39,6 +39,7 @@ import org.lowcoder.api.permission.view.PermissionItemView; import org.lowcoder.api.usermanagement.OrgDevChecker; import org.lowcoder.domain.application.model.Application; +import org.lowcoder.domain.application.model.ApplicationRequestType; import org.lowcoder.domain.application.model.ApplicationStatus; import org.lowcoder.domain.application.model.ApplicationType; import org.lowcoder.domain.application.service.ApplicationService; @@ -73,10 +74,12 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +@RequiredArgsConstructor @Service @Slf4j public class ApplicationApiService { @@ -85,54 +88,25 @@ public class ApplicationApiService { private static final String JS_DATASOURCE_TYPE = "js"; private static final String VIEW_DATASOURCE_TYPE = "view"; - @Autowired - private ApplicationService applicationService; - - @Autowired - private ResourcePermissionService resourcePermissionService; - - @Autowired - private SessionUserService sessionUserService; - - @Autowired - private OrgMemberService orgMemberService; - - @Autowired - private GroupService groupService; - - @Autowired - private OrganizationService organizationService; - - @Autowired - private UserService userService; - - @Autowired - private AbstractBizThresholdChecker bizThresholdChecker; - - @Autowired - private TemplateSolution templateSolution; - - @Autowired - private SuggestAppAdminSolution suggestAppAdminSolution; - - @Autowired - private OrgDevChecker orgDevChecker; - @Autowired - private FolderApiService folderApiService; - @Autowired - private UserHomeApiService userHomeApiService; - @Autowired - private UserApplicationInteractionService userApplicationInteractionService; - @Autowired - private DatasourceMetaInfoService datasourceMetaInfoService; - @Autowired - private CompoundApplicationDslFilter compoundApplicationDslFilter; - @Autowired - private TemplateService templateService; - @Autowired - private PermissionHelper permissionHelper; - @Autowired - private DatasourceService datasourceService; + private final ApplicationService applicationService; + private final ResourcePermissionService resourcePermissionService; + private final SessionUserService sessionUserService; + private final OrgMemberService orgMemberService; + private final OrganizationService organizationService; + + private final AbstractBizThresholdChecker bizThresholdChecker; + private final OrgDevChecker orgDevChecker; + private final TemplateSolution templateSolution; + private final SuggestAppAdminSolution suggestAppAdminSolution; + + private final FolderApiService folderApiService; + private final UserHomeApiService userHomeApiService; + private final UserApplicationInteractionService userApplicationInteractionService; + private final DatasourceMetaInfoService datasourceMetaInfoService; + private final CompoundApplicationDslFilter compoundApplicationDslFilter; + private final TemplateService templateService; + private final PermissionHelper permissionHelper; + private final DatasourceService datasourceService; public Mono create(CreateApplicationRequest createApplicationRequest) { @@ -141,7 +115,8 @@ public Mono create(CreateApplicationRequest createApplicationRe createApplicationRequest.applicationType(), NORMAL, createApplicationRequest.publishedApplicationDSL(), - false, false, false, createApplicationRequest.editingApplicationDSL()); + createApplicationRequest.editingApplicationDSL(), + false, false, false); if (StringUtils.isBlank(application.getOrganizationId())) { return deferredError(INVALID_PARAMETER, "ORG_ID_EMPTY"); @@ -248,22 +223,22 @@ private Mono checkApplicationStatus(Application application, ApplicationSt return Mono.error(new BizException(BizError.UNSUPPORTED_OPERATION, "BAD_REQUEST")); } - private Mono checkApplicationViewRequest(Application application, ApplicationEndpoints.ApplicationRequestType expected) { + private Mono checkApplicationViewRequest(Application application, ApplicationRequestType expected) { // TODO: The check is correct ( logically ) but we need to provide some time for the users to adapt. Will bring it back in the next release // Falk: switched && application.isPublicToAll() on again - seems here is the bug. - if (expected == ApplicationEndpoints.ApplicationRequestType.PUBLIC_TO_ALL && application.isPublicToAll()) { + if (expected == ApplicationRequestType.PUBLIC_TO_ALL && application.isPublicToAll()) { return Mono.empty(); } // Falk: here is to check the ENV Variable LOWCODER_MARKETPLACE_PRIVATE_MODE // isPublicToMarketplace & isPublicToAll must be both true - if (expected == ApplicationEndpoints.ApplicationRequestType.PUBLIC_TO_MARKETPLACE && application.isPublicToMarketplace() && application.isPublicToAll()) { + if (expected == ApplicationRequestType.PUBLIC_TO_MARKETPLACE && application.isPublicToMarketplace() && application.isPublicToAll()) { return Mono.empty(); } // // Falk: application.agencyProfile() & isPublicToAll must be both true - if (expected == ApplicationEndpoints.ApplicationRequestType.AGENCY_PROFILE && application.agencyProfile() && application.isPublicToAll()) { + if (expected == ApplicationRequestType.AGENCY_PROFILE && application.agencyProfile() && application.isPublicToAll()) { return Mono.empty(); } return Mono.error(new BizException(BizError.UNSUPPORTED_OPERATION, "BAD_REQUEST")); @@ -301,8 +276,8 @@ public Mono getEditingApplication(String applicationId) { }); } - public Mono getPublishedApplication(String applicationId, ApplicationEndpoints.ApplicationRequestType requestType) { - return checkPermissionWithReadableErrorMsg(applicationId, READ_APPLICATIONS) + public Mono getPublishedApplication(String applicationId, ApplicationRequestType requestType) { + return checkApplicationPermissionWithReadableErrorMsg(applicationId, READ_APPLICATIONS, requestType) .zipWhen(permission -> applicationService.findById(applicationId) .delayUntil(application -> checkApplicationStatus(application, NORMAL)) .delayUntil(application -> checkApplicationViewRequest(application, requestType))) @@ -493,6 +468,32 @@ public Mono checkPermissionWithReadableErrorMsg(String appli }); } + @Nonnull + public Mono checkApplicationPermissionWithReadableErrorMsg(String applicationId, ResourceAction action, ApplicationRequestType requestType) { + return sessionUserService.getVisitorId() + .flatMap(visitorId -> resourcePermissionService.checkUserPermissionStatusOnApplication(visitorId, applicationId, action, requestType)) + .flatMap(permissionStatus -> { + if (!permissionStatus.hasPermission()) { + if (permissionStatus.failByAnonymousUser()) { + return ofError(USER_NOT_SIGNED_IN, "USER_NOT_SIGNED_IN"); + } + + if (permissionStatus.failByNotInOrg()) { + return ofError(NO_PERMISSION_TO_REQUEST_APP, "INSUFFICIENT_PERMISSION"); + } + + return suggestAppAdminSolution.getSuggestAppAdminNames(applicationId) + .flatMap(names -> { + String messageKey = action == EDIT_APPLICATIONS ? "NO_PERMISSION_TO_EDIT" : "NO_PERMISSION_TO_VIEW"; + return ofError(NO_PERMISSION_TO_REQUEST_APP, messageKey, names); + }); + } + return Mono.just(permissionStatus.getPermission()); + }); + } + + + private ApplicationInfoView buildView(Application application, String role) { return buildView(application, role, null); } 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 5270b9a19..d12297b33 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 @@ -6,7 +6,6 @@ import static org.lowcoder.infra.event.EventType.APPLICATION_RECYCLED; import static org.lowcoder.infra.event.EventType.APPLICATION_RESTORE; import static org.lowcoder.infra.event.EventType.APPLICATION_UPDATE; -import static org.lowcoder.infra.event.EventType.VIEW; import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; @@ -17,16 +16,17 @@ import org.lowcoder.api.application.view.ApplicationView; import org.lowcoder.api.application.view.MarketplaceApplicationInfoView; // should we not have a AgencyApplicationInfoView - import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.api.home.SessionUserService; import org.lowcoder.api.home.UserHomeApiService; import org.lowcoder.api.home.UserHomepageView; import org.lowcoder.api.util.BusinessEventPublisher; import org.lowcoder.domain.application.model.Application; +import org.lowcoder.domain.application.model.ApplicationRequestType; import org.lowcoder.domain.application.model.ApplicationStatus; import org.lowcoder.domain.application.model.ApplicationType; import org.lowcoder.domain.permission.model.ResourceRole; +import org.lowcoder.infra.event.EventType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -98,7 +98,7 @@ public Mono> getEditingApplication(@PathVariable S public Mono> getPublishedApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_ALL) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) .map(ResponseView::success); } @@ -106,7 +106,7 @@ public Mono> getPublishedApplication(@PathVariable public Mono> getPublishedMarketPlaceApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_MARKETPLACE) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) .map(ResponseView::success); } @@ -114,7 +114,7 @@ public Mono> getPublishedMarketPlaceApplication(@P public Mono> getAgencyProfileApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.AGENCY_PROFILE) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) .map(ResponseView::success); } 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 4df112274..b026a2544 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 @@ -289,12 +289,6 @@ public Boolean agencyProfile() { } } - public enum ApplicationRequestType { - PUBLIC_TO_ALL, - PUBLIC_TO_MARKETPLACE, - AGENCY_PROFILE, - } - public record UpdatePermissionRequest(String role) { } diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceTest.java index 5bf57b461..0a53e58a7 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/application/ApplicationApiServiceTest.java @@ -8,7 +8,6 @@ import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; -import org.lowcoder.api.application.ApplicationEndpoints.ApplicationRequestType; import org.lowcoder.api.application.ApplicationEndpoints.CreateApplicationRequest; import org.lowcoder.api.application.view.ApplicationPermissionView; import org.lowcoder.api.application.view.ApplicationView; @@ -17,6 +16,7 @@ import org.lowcoder.api.home.FolderApiService; import org.lowcoder.api.permission.view.PermissionItemView; import org.lowcoder.domain.application.model.Application; +import org.lowcoder.domain.application.model.ApplicationRequestType; import org.lowcoder.domain.application.model.ApplicationStatus; import org.lowcoder.domain.application.model.ApplicationType; import org.lowcoder.domain.application.service.ApplicationService; From de03572ed25cce569fcec24a42d33664f6446127 Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Fri, 1 Mar 2024 10:05:02 +0100 Subject: [PATCH 29/32] fix: update location of marketplace app settings --- .../domain/application/model/Application.java | 4 ++++ .../api/home/UserHomeApiServiceImpl.java | 17 +++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) 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 c102c5f55..2b73637cc 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 @@ -167,4 +167,8 @@ public Object getLiveContainerSize() { return liveContainerSize.get(); } + public Map getPublishedApplicationDSL() { + return publishedApplicationDSL; + } + } 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 95f62de61..c682e8c70 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 @@ -6,6 +6,7 @@ import static org.lowcoder.sdk.util.StreamUtils.collectList; import java.time.Instant; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -316,12 +317,16 @@ public Flux getAllMarketplaceApplications(@Nulla .build(); // marketplace specific fields - Map marketplaceMeta = (Map) - ((Map)application.getEditingApplicationDSL().get("ui")).get("marketplaceMeta"); - marketplaceApplicationInfoView.setTitle((String)marketplaceMeta.get("title")); - marketplaceApplicationInfoView.setCategory((String)marketplaceMeta.get("category")); - marketplaceApplicationInfoView.setDescription((String)marketplaceMeta.get("description")); - marketplaceApplicationInfoView.setImage((String)marketplaceMeta.get("image")); + Map settings = new HashMap<>(); + if (application.getPublishedApplicationDSL() != null) + { + settings.putAll((Map)application.getPublishedApplicationDSL().getOrDefault("settings", new HashMap<>())); + } + + marketplaceApplicationInfoView.setTitle((String)settings.getOrDefault("title", application.getName())); + marketplaceApplicationInfoView.setCategory((String)settings.get("category")); + marketplaceApplicationInfoView.setDescription((String)settings.get("description")); + marketplaceApplicationInfoView.setImage((String)settings.get("icon")); return marketplaceApplicationInfoView; From d5c9c32d9404e3a75975d6f5876ec5fed9b2c2ef Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Fri, 1 Mar 2024 14:27:38 +0100 Subject: [PATCH 30/32] fix: for now, allow to view non-published apps for logged in users --- .../repository/ApplicationRepository.java | 13 +--------- .../service/ApplicationService.java | 24 +++++++++++++++++-- .../service/ApplicationPermissionHandler.java | 11 +++++---- .../application/ApplicationApiService.java | 12 +++++----- 4 files changed, 36 insertions(+), 24 deletions(-) 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 c75402472..de6b069ef 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 @@ -2,7 +2,6 @@ import java.util.Collection; -import java.util.List; import javax.annotation.Nonnull; @@ -33,17 +32,7 @@ public interface ApplicationRepository extends ReactiveMongoRepository findByDatasourceId(String datasourceId); - Flux findByIdIn(List ids); - - - // Falk: Why to combine? Marketplace-List and Agency-List are different Endpoints - - /* @Query(value = "{$and:[{'publicToAll':true},{'$or':[{'publicToMarketplace':?0},{'agencyProfile':?1}]}, {'_id': { $in: ?2}}]}", fields = "{_id : 1}") - Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsOrAgencyProfileIsAndIdIn - (Boolean publicToMarketplace, Boolean agencyProfile, Collection ids); */ - - // this we do not need - // Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsAndAgencyProfileIsAndIdIn(Boolean publicToMarketplace, Boolean agencyProfile, Collection ids); + Flux findByIdIn(Collection ids); /** * Filter public applications from list of supplied IDs diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java index e6e9b9b39..a5f83cdaf 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationService.java @@ -206,12 +206,19 @@ public Mono setApplicationAsAgencyProfile(String applicationId, boolean @NonEmptyMono @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") - public Mono> getFilteredPublicApplicationIds(ApplicationRequestType requestType, Collection applicationIds, Boolean isAnonymous, Boolean isPrivateMarketplace) { + public Mono> getFilteredPublicApplicationIds(ApplicationRequestType requestType, Collection applicationIds, boolean isAnonymous, Boolean isPrivateMarketplace) { switch(requestType) { case PUBLIC_TO_ALL: - return getPublicApplicationIds(applicationIds); + if (isAnonymous) + { + return getPublicApplicationIds(applicationIds); + } + else + { + return getPrivateApplicationIds(applicationIds); + } case PUBLIC_TO_MARKETPLACE: return getPublicMarketplaceApplicationIds(applicationIds, isAnonymous, isPrivateMarketplace); case AGENCY_PROFILE: @@ -235,6 +242,19 @@ public Mono> getPublicApplicationIds(Collection applicationI } + /** + * Find all private applications for viewing. + */ + @NonEmptyMono + @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") + public Mono> getPrivateApplicationIds(Collection applicationIds) { + // TODO: in 2.4.0 we need to check whether the app was published or not + return repository.findByIdIn(applicationIds) + .map(HasIdAndAuditing::getId) + .collect(Collectors.toSet()); + } + + /** * Find all marketplace applications - filter based on whether user is anonymous and whether it's a private marketplace */ diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ApplicationPermissionHandler.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ApplicationPermissionHandler.java index 00bd9d987..c97c2b236 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ApplicationPermissionHandler.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ApplicationPermissionHandler.java @@ -10,6 +10,7 @@ import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -62,7 +63,7 @@ protected Mono>> getAnonymousUserPermission (Collection resourceIds, ResourceAction resourceAction) { Set applicationIds = newHashSet(resourceIds); - return Mono.zip(applicationService.getPublicApplicationIds(applicationIds), + return Mono.zip(applicationService.getPrivateApplicationIds(applicationIds), templateSolution.getTemplateApplicationIds(applicationIds)) .map(tuple -> { Set publicAppIds = tuple.getT1(); @@ -81,9 +82,11 @@ protected Mono>> getAnonymousUserApplicatio } Set applicationIds = newHashSet(resourceIds); - return Mono.zip(applicationService.getFilteredPublicApplicationIds(requestType, applicationIds, Boolean.TRUE, config.getMarketplace().isPrivateMode()), - templateSolution.getTemplateApplicationIds(applicationIds)) - .map(tuple -> { + return Mono.zip(applicationService.getFilteredPublicApplicationIds(requestType, applicationIds, Boolean.TRUE, config.getMarketplace().isPrivateMode()) + .defaultIfEmpty(new HashSet<>()), + templateSolution.getTemplateApplicationIds(applicationIds) + .defaultIfEmpty(new HashSet<>()) + ).map(tuple -> { Set publicAppIds = tuple.getT1(); Set templateAppIds = tuple.getT2(); return collectMap(union(publicAppIds, templateAppIds), identity(), this::getAnonymousUserPermission); 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 c2a23b11a..ea81c199c 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 @@ -224,18 +224,18 @@ private Mono checkApplicationStatus(Application application, ApplicationSt } private Mono checkApplicationViewRequest(Application application, ApplicationRequestType expected) { - // TODO: The check is correct ( logically ) but we need to provide some time for the users to adapt. Will bring it back in the next release - // Falk: switched && application.isPublicToAll() on again - seems here is the bug. - if (expected == ApplicationRequestType.PUBLIC_TO_ALL && application.isPublicToAll()) { + // TODO: check application.isPublicToAll() from v2.4.0 + if (expected == ApplicationRequestType.PUBLIC_TO_ALL) { return Mono.empty(); } // Falk: here is to check the ENV Variable LOWCODER_MARKETPLACE_PRIVATE_MODE // isPublicToMarketplace & isPublicToAll must be both true - if (expected == ApplicationRequestType.PUBLIC_TO_MARKETPLACE && application.isPublicToMarketplace() && application.isPublicToAll()) { - return Mono.empty(); - } + if (expected == ApplicationRequestType.PUBLIC_TO_MARKETPLACE && application.isPublicToMarketplace() && application.isPublicToAll()) { + return Mono.empty(); + } + // // Falk: application.agencyProfile() & isPublicToAll must be both true if (expected == ApplicationRequestType.AGENCY_PROFILE && application.agencyProfile() && application.isPublicToAll()) { From 35472c035930f523ac27475b99ee30d41c4f532f Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Mon, 4 Mar 2024 15:22:52 +0100 Subject: [PATCH 31/32] Adding mn height for App Cards in Marketplace --- .../lowcoder/src/pages/ApplicationV2/MarketplaceResCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceResCard.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceResCard.tsx index 80f4d5667..9f3f6881e 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceResCard.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceResCard.tsx @@ -67,6 +67,7 @@ const Card = styled.div` display: flex; align-items: center; height: 100%; + min-height:100px; width: 100%; border-bottom: 1px solid #f5f5f6; padding: 0 10px; From f31c4f9acd79f23718b62dad7c7bc3f6dd26fa34 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Mon, 4 Mar 2024 18:13:02 +0100 Subject: [PATCH 32/32] Increase Lowcoder Comps Version --- client/packages/lowcoder-comps/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder-comps/package.json b/client/packages/lowcoder-comps/package.json index fe4d32f98..01733833e 100644 --- a/client/packages/lowcoder-comps/package.json +++ b/client/packages/lowcoder-comps/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-comps", - "version": "0.0.25", + "version": "0.0.26", "type": "module", "license": "MIT", "dependencies": {