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/client/packages/lowcoder-comps/package.json b/client/packages/lowcoder-comps/package.json index 43386ccf9..01733833e 100644 --- a/client/packages/lowcoder-comps/package.json +++ b/client/packages/lowcoder-comps/package.json @@ -1,15 +1,19 @@ { "name": "lowcoder-comps", - "version": "0.0.24", + "version": "0.0.26", "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", "@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", diff --git a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx index 72f292ece..c65de7873 100644 --- a/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/calendarComp/calendarComp.tsx @@ -26,6 +26,10 @@ 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 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"; @@ -35,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, @@ -52,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), @@ -66,6 +78,7 @@ const childrenMap = { dayMaxEvents: withDefault(NumberControl, 2), eventMaxStack: withDefault(NumberControl, 0), style: styleControl(CalendarStyle), + licenceKey: withDefault(StringControl, ""), }; let CalendarBasicComp = (function () { @@ -83,14 +96,17 @@ 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), }; }); const { defaultDate, - defaultView, + defaultFreeView, + defaultPremiumView, showEventTime, showWeekends, showAllDay, @@ -99,13 +115,19 @@ let CalendarBasicComp = (function () { style, firstDay, editable, + licenceKey, } = props; 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 +159,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 +219,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 +235,18 @@ let CalendarBasicComp = (function () { } name="id" - rules={[{ required: true, message: trans("calendar.eventIdRequire") }]} + rules={[ + { required: true, message: trans("calendar.eventIdRequire") }, + ]} > @@ -239,9 +269,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) { @@ -287,6 +321,10 @@ let CalendarBasicComp = (function () { } catch (error) { initialDate = undefined; } + let defaultView = defaultFreeView; + if (licenceKey != "") { + defaultView = defaultPremiumView; + } return ( { let left = 0; @@ -319,15 +366,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={licenceKey} views={views} eventClassNames={() => (!showEventTime ? "no-time" : "")} slotLabelFormat={slotLabelFormat} @@ -346,7 +397,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; @@ -385,10 +438,18 @@ let CalendarBasicComp = (function () { ); }) .setPropertyViewFn((children) => { + let licence = children.licenceKey.getView(); 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"), @@ -397,10 +458,15 @@ let CalendarBasicComp = (function () { 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"), })} @@ -424,8 +490,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 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 9cac1339b..18c022991 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,153 @@ 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", + timeline: "TimeLine", //added by fred + 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", }, }; 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/comps/comps/appSettingsComp.tsx b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx index 9fa023cae..1782120df 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: cat + } + } +) + 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/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 0f83804f7..3a00a887d 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/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/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/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); diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx index c7198c17c..f8b154e42 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,11 +79,15 @@ export const getStyle = (style: CheckboxStyleType) => { } } + + .ant-checkbox-group-item { font-family:${style.fontFamily}; 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 a1ea41fe6..11bfceed0 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/selectInputComp/segmentedControl.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx index 156c83799..73a7d4675 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/comps/selectInputComp/selectCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx index 51c525fd8..1eac631ac 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx @@ -76,6 +76,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; @@ -268,7 +270,6 @@ export const SelectUIView = ( label={option.label} disabled={option.disabled} key={option.value} - style={{fontFamily:"Montserrat"}} > {props.options.findIndex((option) => hasIcon(option.prefixIcon)) > 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/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/comps/textComp.tsx b/client/packages/lowcoder/src/comps/comps/textComp.tsx index 4dc46d0f6..fd9ce1f18 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 c760e054b..51815260f 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx @@ -58,7 +58,14 @@ 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}; + text-transform:${(props)=>props.$style.textTransform}; + text-decoration:${(props)=>props.$style.textDecoration}; + margin: 0px 3px 0px 3px !important; + } .ant-input-clear-icon { opacity: 0.45; @@ -196,7 +203,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/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 69d040151..1d01266af 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -248,6 +248,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 8b81237e4..51c88abd7 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", @@ -350,6 +358,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"), @@ -381,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", @@ -399,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, @@ -476,7 +504,6 @@ function replaceAndMergeMultipleStyles(originalArray: any[], styleToReplace: str } export const ButtonStyle = [ - // ...getBgBorderRadiusByBg("primary"), getBackground('primary'), ...STYLING_FIELDS_SEQUENCE ] as const; @@ -527,6 +554,7 @@ export const MarginStyle = [ export const ContainerStyle = [ // ...BG_STATIC_BORDER_RADIUS, getStaticBorder(), + // ...STYLING_FIELDS_SEQUENCE.filter((style) => style.name !== 'border'), getBackground(), RADIUS, BORDER_WIDTH, @@ -729,23 +757,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"), @@ -776,13 +793,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"), @@ -790,7 +808,6 @@ export const TabContainerStyle = [ depType: DEP_TYPE.SELF, transformer: toSelf, }, - ...ContainerStyle, ] as const; export const ModalStyle = [ @@ -838,7 +855,6 @@ function checkAndUncheck() { } export const CheckboxStyle = [ - // LABEL, ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'text', [LABEL, STATIC_TEXT, VALIDATE]).filter((style) => style.name !== 'border'), ...checkAndUncheck(), { @@ -848,15 +864,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(), { @@ -866,14 +877,12 @@ export const RadioStyle = [ depType: DEP_TYPE.SELF, transformer: toSelf, }, - // STATIC_TEXT, - // VALIDATE, - // MARGIN, - // PADDING, + HOVER_BACKGROUND_COLOR ] as const; export const SegmentStyle = [ LABEL, + ...STYLING_FIELDS_SEQUENCE.filter((style)=> ['border','borderWidth'].includes(style.name) === false), { name: "indicatorBackground", label: trans("style.indicatorBackground"), @@ -892,10 +901,7 @@ export const SegmentStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, }, - RADIUS, VALIDATE, - MARGIN, - PADDING, ] as const; const LinkTextStyle = [ @@ -1089,7 +1095,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/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); }} 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..8b80e89b9 --- /dev/null +++ b/client/packages/lowcoder/src/comps/hooks/toastComp.ts @@ -0,0 +1,95 @@ +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, dismissible } = options; + + const closeIcon: boolean | undefined = dismissible === true ? undefined : (dismissible === false ? false : undefined); + + 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, + placement: placement as NotificationPlacement ?? "bottomRight", + closeIcon: closeIcon as boolean, + }; + + // Use notificationArgs to trigger the notification + + 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, []); + +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) => { + 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/constants/applicationConstants.ts b/client/packages/lowcoder/src/constants/applicationConstants.ts index 2e09ba8c3..895d78153 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", @@ -78,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/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index cc41a8d1e..ac395eb65 100644 --- a/client/packages/lowcoder/src/i18n/locales/de.ts +++ b/client/packages/lowcoder/src/i18n/locales/de.ts @@ -324,6 +324,9 @@ export const de = { "tableCellText": "Zelle Text", "selectedRowBackground": "Ausgewählter Zeilenhintergrund", "hoverRowBackground": "Hover Row Hintergrund", + "hoverBackground":"Hover-Hintergrund", + "textTransform":"Texttransformation", + "textDecoration":"Textdekoration", "alternateRowBackground": "Alternativer Reihenhintergrund", "tableHeaderBackground": "Kopfzeile Hintergrund", "tableHeaderText": "Überschrift Text", @@ -1612,10 +1615,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" }, @@ -1849,7 +1860,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..19f6daf67 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -345,6 +345,9 @@ export const en = { "tableCellText": "Cell Text", "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", @@ -1578,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" @@ -1775,6 +1778,15 @@ 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": { + "destroy": "close a Notification", + "info": "Send a Notification", + "loading": "Send a Loading Notification", "success": "Send a Success Notification", "warn": "Send a Warning Notification", "error": "Send an Error Notification" @@ -2033,7 +2045,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", @@ -2251,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", @@ -2606,7 +2622,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 12821ebf6..d2e868a61 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -334,6 +334,9 @@ style: { tableCellText: "单元格文本", selectedRowBackground: "选中行背景", hoverRowBackground: "悬停行背景", + hoverBackground:"悬停背景", + textTransform:"文本变换", + textDecoration:"文字装饰", alternateRowBackground: "交替行背景", tableHeaderBackground: "表头背景", tableHeaderText: "表头文本", @@ -1493,7 +1496,7 @@ settings: { advanced: "高级", lab: "实验室", branding: "品牌", - oauthProviders: "OAuth 提供商", + oauthProviders: "User Authentication", appUsage: "应用程序使用日志", environments: "环境", premium: "高级版", @@ -1683,6 +1686,14 @@ utilsComp: { }, messageComp: { info: "发送通知", + loading: "发送加载通知", + success: "发送成功通知", + warn: "发送警告通知", + error: "发送错误通知", +}, +toastComp: { + info: "发送通知", + loading: "发送加载通知", success: "发送成功通知", warn: "发送警告通知", error: "发送错误通知", @@ -1922,6 +1933,9 @@ appSetting: { themeSetting: "主题设置", themeSettingDefault: "默认", themeCreate: "创建主题", + appTitle: "标题", + appDescription: "描述", + appCategory: "类别", }, customShortcut: { title: "自定义快捷键", @@ -2196,6 +2210,7 @@ home: { "errorMarketplaceApps": "获取市场应用程序错误", "localMarketplaceTitle": "本地市场", "globalMarketplaceTitle": "Lowcoder 市场", + "allCategories": "所有类别", memberPermissionList: "成员权限:", orgName: "{orgName}管理员", addMember: "添加成员", @@ -2546,7 +2561,7 @@ componentDocExtra: { table: table, }, idSource: { - title: "OAuth 提供商", + title: "用户认证提供商", form: "电子邮件", pay: "高级", enable: "启用", 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)) { 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..9f3f6881e --- /dev/null +++ b/client/packages/lowcoder/src/pages/ApplicationV2/MarketplaceResCard.tsx @@ -0,0 +1,195 @@ +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%; + min-height:100px; + 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/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/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/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); 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; } `; 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 ( - { diff --git a/client/yarn.lock b/client/yarn.lock index 6e5ea2fd1..8e014c0d3 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2543,6 +2543,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" @@ -2561,6 +2572,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" @@ -2589,6 +2609,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" @@ -2600,6 +2629,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" @@ -2611,6 +2703,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" @@ -11834,12 +11949,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 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` | diff --git a/docs/.gitbook/assets/builtin-js-toasts.png b/docs/.gitbook/assets/builtin-js-toasts.png new file mode 100644 index 000000000..cfaf58332 Binary files /dev/null and b/docs/.gitbook/assets/builtin-js-toasts.png differ 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..0bbbb6db0 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,45 @@ 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. 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", 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", 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", 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", 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", dismissible?: boolean = true } ) +toast.error("Your credentials were invalid", {message: "You have 5 tries left", duration: 5}) +//toast.destroy(id?: string) +toast.destroy() +``` + +
+ ## 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`. 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..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 @@ -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; @@ -166,4 +167,8 @@ public Object getLiveContainerSize() { return liveContainerSize.get(); } + public Map getPublishedApplicationDSL() { + return publishedApplicationDSL; + } + } 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 7c42ccc73..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; @@ -18,6 +17,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); @@ -32,16 +32,30 @@ public interface ApplicationRepository extends ReactiveMongoRepository findByDatasourceId(String datasourceId); - Flux findByIdIn(List ids); + Flux findByIdIn(Collection ids); - @Query(value = "{$and:[{'publicToAll':true},{'$or':[{'publicToMarketplace':?0},{'agencyProfile':?1}]}, {'_id': { $in: ?2}}]}", fields = "{_id : 1}") - Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsOrAgencyProfileIsAndIdIn - (Boolean publicToMarketplace, Boolean agencyProfile, Collection ids); + /** + * Filter public applications from list of supplied IDs + */ + Flux findByPublicToAllIsTrueAndIdIn(Collection ids); - Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsAndAgencyProfileIsAndIdIn(Boolean publicToMarketplace, Boolean agencyProfile, Collection ids); + /** + * Filter marketplace applications from list of supplied IDs + */ + Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsTrueAndIdIn(Collection ids); - Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsTrue(); + /** + * Filter agency applications from list of supplied IDs + */ + Flux findByPublicToAllIsTrueAndAgencyProfileIsTrueAndIdIn(Collection ids); + /** + * Find all marketplace applications + */ + Flux findByPublicToAllIsTrueAndPublicToMarketplaceIsTrue(); + + /** + * 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 dfc3f2a8d..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 @@ -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; @@ -152,13 +153,17 @@ 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) + .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 +183,7 @@ public Mono setApplicationPublicToMarketplace(String applicationId, Boo applicationDsl.replace("ui", dataObject); - } + } */ return Application.builder() .publicToMarketplace(publicToMarketplace) @@ -198,27 +203,84 @@ public Mono setApplicationAsAgencyProfile(String applicationId, boolean return mongoUpsertHelper.updateById(application, applicationId); } + + @NonEmptyMono + @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") + public Mono> getFilteredPublicApplicationIds(ApplicationRequestType requestType, Collection applicationIds, boolean isAnonymous, Boolean isPrivateMarketplace) { + + switch(requestType) + { + case PUBLIC_TO_ALL: + if (isAnonymous) + { + return getPublicApplicationIds(applicationIds); + } + else + { + return getPrivateApplicationIds(applicationIds); + } + case PUBLIC_TO_MARKETPLACE: + return getPublicMarketplaceApplicationIds(applicationIds, isAnonymous, isPrivateMarketplace); + case AGENCY_PROFILE: + return getPublicAgencyApplicationIds(applicationIds); + default: + return Mono.empty(); + } + } + + + /** + * Find all public applications - doesn't matter if user is anonymous, because these apps are public + */ @NonEmptyMono @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") - public Mono> getPublicApplicationIds(Collection applicationIds, Boolean isAnonymous, Boolean isPrivateMarketplace) { + public Mono> getPublicApplicationIds(Collection applicationIds) { - if(isAnonymous) { - if(isPrivateMarketplace) { - return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsAndAgencyProfileIsAndIdIn(false, false, applicationIds) - .map(HasIdAndAuditing::getId) - .collect(Collectors.toSet()); - } else { - return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsAndAgencyProfileIsAndIdIn(true, false, applicationIds) + return repository.findByPublicToAllIsTrueAndIdIn(applicationIds) .map(HasIdAndAuditing::getId) .collect(Collectors.toSet()); - } - } else { - return repository.findByPublicToAllIsTrueAndPublicToMarketplaceIsOrAgencyProfileIsAndIdIn(true, true, applicationIds) + } + + + /** + * 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 + */ + @NonEmptyMono + @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") + 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(); + } + /** + * Find all agency applications + */ + @NonEmptyMono + @SuppressWarnings("ReactiveStreamsNullableInLambdaInTransform") + 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..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,11 +10,13 @@ import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; 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 +48,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 +63,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.getPrivateApplicationIds(applicationIds), templateSolution.getTemplateApplicationIds(applicationIds)) .map(tuple -> { Set publicAppIds = tuple.getT1(); @@ -70,7 +72,41 @@ 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()) + .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); + }); + } + + @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-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); 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..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 @@ -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,15 +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) { - // 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() */) { + private Mono checkApplicationViewRequest(Application application, ApplicationRequestType expected) { + + // TODO: check application.isPublicToAll() from v2.4.0 + if (expected == ApplicationRequestType.PUBLIC_TO_ALL) { return Mono.empty(); } - if (expected == ApplicationEndpoints.ApplicationRequestType.PUBLIC_TO_MARKETPLACE && application.isPublicToMarketplace()) { + + // 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 == ApplicationEndpoints.ApplicationRequestType.AGENCY_PROFILE && application.agencyProfile()) { + + // + // Falk: application.agencyProfile() & isPublicToAll must be both true + if (expected == ApplicationRequestType.AGENCY_PROFILE && application.agencyProfile() && application.isPublicToAll()) { return Mono.empty(); } return Mono.error(new BizException(BizError.UNSUPPORTED_OPERATION, "BAD_REQUEST")); @@ -294,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))) @@ -445,6 +427,7 @@ public Mono getApplicationPermissions(String applicat .orgName(organization.getName()) .publicToAll(application.isPublicToAll()) .publicToMarketplace(application.isPublicToMarketplace()) + .agencyProfile(application.agencyProfile()) .build(); }); }); @@ -485,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); } @@ -502,6 +511,7 @@ private ApplicationInfoView buildView(Application application, String role, @Nul .folderId(folderId) .publicToAll(application.isPublicToAll()) .publicToMarketplace(application.isPublicToMarketplace()) + .agencyProfile(application.agencyProfile()) .build(); } @@ -519,13 +529,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/ApplicationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java index 86be1e576..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; @@ -16,15 +15,18 @@ 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; 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; @@ -91,11 +93,12 @@ 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) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) .map(ResponseView::success); } @@ -103,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); } @@ -111,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 2ac323289..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 @@ -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) { @@ -306,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/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..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; @@ -399,7 +404,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()) 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;