diff --git a/client/VERSION b/client/VERSION index b0f6bf0cd..e393c3c55 100644 --- a/client/VERSION +++ b/client/VERSION @@ -1 +1 @@ -2.4.10 +2.4.11 \ No newline at end of file diff --git a/client/package.json b/client/package.json index eaf135493..e82e56ed9 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-frontend", - "version": "2.4.9", + "version": "2.4.11", "type": "module", "private": true, "workspaces": [ diff --git a/client/packages/lowcoder-design/src/components/colorSelect/colorUtils.ts b/client/packages/lowcoder-design/src/components/colorSelect/colorUtils.ts index ac24c5900..94621fcb1 100644 --- a/client/packages/lowcoder-design/src/components/colorSelect/colorUtils.ts +++ b/client/packages/lowcoder-design/src/components/colorSelect/colorUtils.ts @@ -4,18 +4,39 @@ import { generate } from "@ant-design/colors/es"; extend([namesPlugin]); +export const gradientColors = [ + "linear-gradient(0deg, #fdfbfb 0%, #ebedee 100%)", + "linear-gradient(45deg, #cfd9df 0%, #e2ebf0 100%)", + "linear-gradient(90deg, #e3ffe7 0%, #d9e7ff 100%)", + + "linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)", + "linear-gradient(0deg, #fbc2eb 0%, #a6c1ee 100%)", + "linear-gradient(45deg, #efd5ff 0%, #515ada 100%)", + "linear-gradient(90deg, #4b6cb7 0%, #72afd3 100%)", + "linear-gradient(135deg, #72afd3 0%, #96e6a1 100%)", + + "linear-gradient(90deg, #fa709a 0%, #fee140 100%)", + "linear-gradient(45deg, #d53369 0%, #daae51 100%)", + "linear-gradient(0deg, #f43b47 0%, #453a94 100%)", + + "linear-gradient(135deg, #00d2ff 0%, #3a47d5 100%)", + "linear-gradient(0deg, #f8ff00 0%, #3ad59f 100%)", + "linear-gradient(45deg, #fcff9e 0%, #c67700 100%)", + "linear-gradient(90deg, #fad0c4 0%, #ffd1ff 100%)", +]; + // Color Palette export const constantColors = [ - { id: 1, color: "#6D83F2" }, - { id: 2, color: "#5589F2" }, - { id: 3, color: "#36B389" }, - { id: 4, color: "#E68E50" }, - { id: 5, color: "#E67373" }, - { id: 6, color: "#F5FFF7" }, - { id: 7, color: "#F3FAFF" }, - { id: 8, color: "#FFF6E6" }, - { id: 9, color: "#F5F5F6" }, - { id: 10, color: "#FFFFFF" }, + "#6D83F2", + "#5589F2", + "#36B389", + "#E68E50", + "#E67373", + "#F5FFF7", + "#F3FAFF", + "#FFF6E6", + "#F5F5F6", + "#FFFFFF", ]; export const chartColorPalette = [ @@ -40,7 +61,17 @@ const alphaOfRgba = (rgba: string) => { return colord(rgba).alpha().toString(); }; -const isValidColor = (str: string) => { +const isValidGradient = (color?: string) => { + if (!color) return false; + + const linearGradientRegex = /^linear-gradient\((\d+deg|to\s+(top|right|bottom|left)(\s+(top|right|bottom|left))?)\s*,\s*((#[0-9a-fA-F]{3,6}|rgba?\(\d+,\s*\d+,\s*\d+(,\s*\d+(\.\d+)?)?\)|[a-zA-Z]+)(\s+\d+%?)?,?\s*)+\)$/i; + const radialGradientRegex = /^radial-gradient\(\s*(circle|ellipse)?\s*,\s*((#[0-9a-fA-F]{3,6}|rgba?\(\d+,\s*\d+,\s*\d+(,\s*\d+(\.\d+)?)?\)|[a-zA-Z]+)(\s+\d+%?)?,?\s*)+\)$/i; + + return linearGradientRegex.test(color) || radialGradientRegex.test(color); +} + +const isValidColor = (str?: string) => { + if (!str) return false; return colord(str).isValid(); }; @@ -91,4 +122,4 @@ export const darkenColor = (colorStr: string, intensity: number) => { return color.darken(intensity).toHex().toUpperCase(); }; -export { toRGBA, toHex, alphaOfRgba, isValidColor }; +export { toRGBA, toHex, alphaOfRgba, isValidColor, isValidGradient }; diff --git a/client/packages/lowcoder-design/src/components/colorSelect/index.tsx b/client/packages/lowcoder-design/src/components/colorSelect/index.tsx index 24c091256..52f2a206f 100644 --- a/client/packages/lowcoder-design/src/components/colorSelect/index.tsx +++ b/client/packages/lowcoder-design/src/components/colorSelect/index.tsx @@ -1,5 +1,5 @@ -import { RgbaStringColorPicker } from "react-colorful"; import { default as Popover } from "antd/es/popover"; +import ColorPicker, {useColorPicker} from 'react-best-gradient-color-picker'; import { ActionType } from '@rc-component/trigger/lib/interface'; import { alphaOfRgba, @@ -7,9 +7,11 @@ import { toHex, constantColors, isValidColor, + isValidGradient, + gradientColors, } from "components/colorSelect/colorUtils"; import styled, { css } from "styled-components"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useRef, useState, useEffect, useMemo, } from "react"; import { throttle } from "lodash"; import { changeValueAction } from "lowcoder-core"; @@ -18,54 +20,65 @@ interface ColorSelectProps { trigger?: ActionType; dispatch?: (value: any) => void; changeColor?: (value: any) => void; + presetColors?: string[]; + allowGradient?: boolean; } export const ColorSelect = (props: ColorSelectProps) => { const { color, trigger = "click", dispatch, changeColor } = props; - let pickerColor = useRef(toRGBA(color)); const [visible, setVisible] = useState(false); + const [ selectedColor, setSelectedColor ] = useState(color); + const { getGradientObject } = useColorPicker(selectedColor, setSelectedColor); + + const presetColors = useMemo(() => { + let colors = props.presetColors || []; + if (props.allowGradient) { + colors = colors.concat(gradientColors.slice(0, 16 - colors.length)); + } + return colors; + }, [props.presetColors, selectedColor, props.allowGradient]); + const throttleChange = useCallback( throttle((rgbaColor: string) => { - dispatch && dispatch(changeValueAction(toHex(rgbaColor), true)); - changeColor && changeColor(toHex(rgbaColor)); + dispatch && dispatch(changeValueAction(rgbaColor, true)); + changeColor && changeColor(rgbaColor); }, 200), [dispatch,changeColor] ); + + useEffect(() => { + if (color !== selectedColor) { + const value = getGradientObject(); + if (!value?.isGradient) { + return throttleChange(toHex(selectedColor)); + } + throttleChange(selectedColor); + } + }, [selectedColor]) + return ( { - pickerColor.current = toRGBA(color); setVisible(value); }} content={ -
- - - - -
- - {constantColors.map((item) => { - return ( - { - throttleChange(item.color); - pickerColor.current = toRGBA(item.color); - }} - /> - ); - })} - +
} > - - +
); @@ -139,7 +152,6 @@ const PopoverContainer = styled.div` display: flex; flex-direction: column; gap: 12px; - padding: 16px; `; // contrast block const AlphaDiv = styled.div.attrs((props) => ({ @@ -169,7 +181,11 @@ const BackDiv = styled.div.attrs<{ $color: string }>((props: { $color: string }) `; // main block const ColorBlock = styled.div<{ $color: string }>` - background-color: ${(props) => (isValidColor(props.$color) ? props.$color : "#FFFFFF")}; + background: ${(props) => ( + isValidColor(props.$color) || isValidGradient(props.$color) + ? props.$color + : "#FFFFFF" + )}; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 4px; height: 24px; @@ -177,4 +193,18 @@ const ColorBlock = styled.div<{ $color: string }>` cursor: pointer; background-clip: content-box; overflow: hidden; +`; + +const StyledColorPicker = styled(ColorPicker)<{$allowGradient?: boolean}>` + #rbgcp-wrapper > div:nth-child(2) > div:first-child > div:first-child { + ${props => !props.$allowGradient && `visibility: hidden`}; + } + #rbgcp-wrapper > div:last-child > div:last-child { + justify-content: flex-start !important; + gap: 3px; + + > div { + border: 1px solid lightgray; + } + } `; \ No newline at end of file diff --git a/client/packages/lowcoder-design/src/icons/index.tsx b/client/packages/lowcoder-design/src/icons/index.tsx index 785b43f1f..687d3516b 100644 --- a/client/packages/lowcoder-design/src/icons/index.tsx +++ b/client/packages/lowcoder-design/src/icons/index.tsx @@ -172,6 +172,8 @@ export { ReactComponent as LeftSettingIcon } from "./remix/tools-fill.svg"; export { ReactComponent as LeftLayersIcon } from "./remix/stack-line.svg"; export { ReactComponent as LeftHelpIcon } from "./v1/icon-left-help.svg"; export { ReactComponent as LeftPreloadIcon } from "./v1/icon-left-preload.svg"; +export { ReactComponent as LeftColorPaletteIcon } from "./remix/palette-line.svg"; +export { ReactComponent as LeftJSSettingIcon } from "./remix/javascript-line.svg"; export { ReactComponent as HomeSettingsIcon } from "./v1/icon-home-settings.svg"; diff --git a/client/packages/lowcoder/package.json b/client/packages/lowcoder/package.json index 2f9133773..d5ef8d284 100644 --- a/client/packages/lowcoder/package.json +++ b/client/packages/lowcoder/package.json @@ -64,6 +64,7 @@ "qrcode.react": "^3.1.0", "rc-trigger": "^5.3.1", "react": "^18.2.0", + "react-best-gradient-color-picker": "^3.0.10", "react-colorful": "^5.5.1", "react-documents": "^1.2.1", "react-dom": "^18.2.0", diff --git a/client/packages/lowcoder/src/api/commonSettingApi.ts b/client/packages/lowcoder/src/api/commonSettingApi.ts index 6b80e4527..510e67026 100644 --- a/client/packages/lowcoder/src/api/commonSettingApi.ts +++ b/client/packages/lowcoder/src/api/commonSettingApi.ts @@ -53,7 +53,16 @@ export interface ThemeDetail { chart?: string; margin?: string; padding?: string; - gridColumns?: string; //Added By Aqib Mirza + gridPaddingX?: number; + gridPaddingY?: number; + gridColumns?: string; + gridRowHeight?: string; + gridRowCount?: number; + gridBgImage?: string; + gridBgImageRepeat?: string; + gridBgImageSize?: string; + gridBgImagePosition?: string; + gridBgImageOrigin?: string; text?: string; textSize?: string; fontFamily?: string; diff --git a/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx b/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx index 54469195a..c81aa9542 100644 --- a/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx +++ b/client/packages/lowcoder/src/components/ThemeSettingsSelector.tsx @@ -1,7 +1,7 @@ import _ from "lodash"; import { useEffect, useState } from "react"; import { ConfigItem, Radius, Margin, Padding, GridColumns, BorderWidth, BorderStyle } from "../pages/setting/theme/styledComponents"; -import { isValidColor, toHex } from "components/colorSelect/colorUtils"; +import { isValidColor, isValidGradient, toHex } from "components/colorSelect/colorUtils"; import { ColorSelect } from "components/colorSelect"; import { TacoInput } from "components/tacoInput"; import { Slider, Switch } from "antd"; @@ -21,7 +21,6 @@ export type configChangeParams = { chart?: string; margin?: string; padding?: string; - gridColumns?: string; // Added By Aqib Mirza borderStyle?: string; borderColor?: string; borderWidth?: string; @@ -29,6 +28,16 @@ export type configChangeParams = { components?: Record, showComponentLoadingIndicators?: boolean; showDataLoadingIndicators?: boolean; + gridColumns?: string; + gridRowHeight?: string; + gridRowCount?: number; + gridPaddingX?: number; + gridPaddingY?: number; + gridBgImage?: string; + gridBgImageRepeat?: string; + gridBgImageSize?: string; + gridBgImagePosition?: string; + gridBgImageOrigin?: string; }; type ColorConfigProps = { @@ -47,9 +56,18 @@ type ColorConfigProps = { fontFamily?: string; margin?: string; padding?: string; - gridColumns?: string; // Added By Aqib Mirza showComponentLoadingIndicators?: boolean; showDataLoadingIndicators?: boolean; + gridColumns?: string; + gridRowHeight?: string; + gridRowCount?: number; + gridPaddingX?: number; + gridPaddingY?: number; + gridBgImage?: string; + gridBgImageRepeat?: string; + gridBgImageSize?: string; + gridBgImagePosition?: string; + gridBgImageOrigin?: string; }; export default function ThemeSettingsSelector(props: ColorConfigProps) { @@ -63,33 +81,54 @@ export default function ThemeSettingsSelector(props: ColorConfigProps) { showVarName = true, margin: defaultMargin, padding: defaultPadding, - gridColumns: defaultGridColumns, borderStyle: defaultBorderStyle, borderWidth: defaultBorderWidth, borderColor: defaultBorderColor, fontFamily: defaultFontFamily, showComponentLoadingIndicators: defaultShowComponentLoaders, showDataLoadingIndicators: defaultShowDataLoaders, + gridColumns: defaultGridColumns, + gridRowHeight: defaultGridRowHeight, + gridRowCount: defaultGridRowCount, + gridPaddingX: defaultGridPaddingX, + gridPaddingY: defaultGridPaddingY, + gridBgImage: defaultGridBgImage, + gridBgImageRepeat: defaultGridBgImageRepeat, + gridBgImageSize: defaultGridBgImageSize, + gridBgImagePosition: defaultGridBgImagePosition, + gridBgImageOrigin: defaultGridBgImageOrigin, } = props; - + const configChangeWithDebounce = _.debounce(configChange, 0); const [color, setColor] = useState(defaultColor); const [radius, setRadius] = useState(defaultRadius); const [margin, setMargin] = useState(defaultMargin); const [padding, setPadding] = useState(defaultPadding); - const [gridColumns, setGridColumns] = useState(defaultGridColumns); const [borderStyle, setBorderStyle] = useState(defaultBorderStyle); const [borderWidth, setBorderWidth] = useState(defaultBorderWidth); const [borderColor, setBorderColor] = useState(defaultBorderColor); const [fontFamily, setFontFamily] = useState(defaultFontFamily); const [showComponentLoaders, setComponentLoaders] = useState(defaultShowComponentLoaders); const [showDataLoaders, setDataLoaders] = useState(defaultShowDataLoaders); + const [gridColumns, setGridColumns] = useState(defaultGridColumns); + const [gridRowHeight, setGridRowHeight] = useState(defaultGridRowHeight); + const [gridRowCount, setGridRowCount] = useState(defaultGridRowCount); + const [gridPaddingX, setGridPaddingX] = useState(defaultGridPaddingX); + const [gridPaddingY, setGridPaddingY] = useState(defaultGridPaddingY); + const [gridBgImage, setGridBgImage] = useState(defaultGridBgImage); + const [gridBgImageRepeat, setGridBgImageRepeat] = useState(defaultGridBgImageRepeat); + const [gridBgImageSize, setGridBgImageSize] = useState(defaultGridBgImageSize); + const [gridBgImagePosition, setGridBgImagePosition] = useState(defaultGridBgImagePosition); + const [gridBgImageOrigin, setGridBgImageOrigin] = useState(defaultGridBgImageOrigin); const varName = `(${themeSettingKey})`; const colorInputBlur = () => { - if (!color || !isValidColor(color)) { + if (!color || !isValidColor(color) || !isValidGradient(color)) { setColor(defaultColor); + } else if (isValidGradient(color)) { + setColor(color); + configChange({ themeSettingKey, color: color }); } else { setColor(toHex(color)); configChange({ themeSettingKey, color: toHex(color) }); @@ -136,20 +175,10 @@ export default function ThemeSettingsSelector(props: ColorConfigProps) { result = padding; } else { result = "3px"; - } - setPadding(result); - configChange({ themeSettingKey, padding: result }); - }; - - const gridColumnsInputBlur = (gridColumns: string) => { - let result = ""; - if (!gridColumns) { - result = "24"; - } else { - result = gridColumns; } - setGridColumns(result); - configChange({ themeSettingKey, gridColumns: result }); + + setPadding(result); + configChange({ themeSettingKey, padding: result }); }; const borderStyleInputBlur = (borderStyle: string) => { @@ -194,9 +223,77 @@ export default function ThemeSettingsSelector(props: ColorConfigProps) { configChange({ themeSettingKey, fontFamily: result }); }; + const gridSizeInputBlur = (value: string) => { + let result = ""; + if (!value) { + result = themeSettingKey === 'gridRowHeight' ? '8' : '24'; + } else { + result = value; + } + if (themeSettingKey === 'gridRowHeight') { + setGridRowHeight(result); + configChange({ themeSettingKey, gridRowHeight: result }); + return; + } + setGridColumns(result); + configChange({ themeSettingKey, gridColumns: result }); + }; + + const gridRowCountInputBlur = (value: string) => { + let result = Infinity; + if (value !== '') { + result = Number(value); + } + + setGridRowCount(result); + configChange({ themeSettingKey, gridRowCount: result }); + }; + + const gridPaddingInputBlur = (padding: string) => { + let result = 20; + if (padding !== '') { + result = Number(padding); + } + + if (themeSettingKey === 'gridPaddingX') { + setGridPaddingX(result); + configChange({ themeSettingKey, gridPaddingX: result }); + return; + } + if (themeSettingKey === 'gridPaddingY') { + setGridPaddingY(result); + configChange({ themeSettingKey, gridPaddingY: result }); + return; + } + }; + + const gridBackgroundInputBlur = (value: string) => { + switch (themeSettingKey) { + case 'gridBgImage': + setGridBgImage(value); + configChange({ themeSettingKey, gridBgImage: value }); + break; + case 'gridBgImageRepeat': + setGridBgImageRepeat(value || 'no-repeat'); + configChange({ themeSettingKey, gridBgImageRepeat: value }); + break; + case 'gridBgImageSize': + setGridBgImageSize(value || "cover"); + configChange({ themeSettingKey, gridBgImageSize: value }); + break; + case 'gridBgImagePosition': + setGridBgImagePosition(value || "center"); + configChange({ themeSettingKey, gridBgImagePosition: value }); + break; + case 'gridBgImageOrigin': + setGridBgImageOrigin(value || 'padding-box'); + configChange({ themeSettingKey, gridBgImageOrigin: value }); + break; + } + } useEffect(() => { - if (color && isValidColor(color)) { + if (color && (isValidColor(color) || isValidGradient(color))) { configChangeWithDebounce({ themeSettingKey, color }); } }, [color]); @@ -221,6 +318,10 @@ export default function ThemeSettingsSelector(props: ColorConfigProps) { setGridColumns(defaultGridColumns); }, [defaultGridColumns]); + useEffect(() => { + setGridRowCount(defaultGridRowCount); + }, [defaultGridRowCount]); + useEffect(() => { setBorderStyle(defaultBorderStyle); }, [defaultBorderStyle]); @@ -245,6 +346,14 @@ export default function ThemeSettingsSelector(props: ColorConfigProps) { setDataLoaders(defaultShowDataLoaders); }, [defaultShowDataLoaders]); + useEffect(() => { + setGridPaddingX(defaultGridPaddingX); + }, [defaultGridPaddingX]); + + useEffect(() => { + setGridPaddingY(defaultGridPaddingY); + }, [defaultGridPaddingY]); + return ( {themeSettingKey !== "showDataLoadingIndicators" @@ -261,20 +370,27 @@ export default function ThemeSettingsSelector(props: ColorConfigProps) { {themeSettingKey !== "radius" && themeSettingKey !== "margin" && themeSettingKey !== "padding" && - themeSettingKey !== "gridColumns" && themeSettingKey !== "borderStyle" && themeSettingKey !== "borderWidth" && themeSettingKey !== "fontFamily" && themeSettingKey !== "showComponentLoadingIndicators" && - themeSettingKey !== "showDataLoadingIndicators" && ( + themeSettingKey !== "showDataLoadingIndicators" && + themeSettingKey !== "gridColumns" && + themeSettingKey !== "gridRowHeight" && + themeSettingKey !== "gridRowCount" && + themeSettingKey !== "gridPaddingX" && + themeSettingKey !== "gridPaddingY" && + themeSettingKey !== "gridBgImage" && + themeSettingKey !== "gridBgImageRepeat" && + themeSettingKey !== "gridBgImageSize" && + themeSettingKey !== "gridBgImagePosition" && + themeSettingKey !== "gridBgImageOrigin" && (
)} - {themeSettingKey === "gridColumns" && ( -
- -
- -
-
- - setGridColumns(value.toString())} - onChangeComplete={(value) => gridColumnsInputBlur(value.toString())} - /> -
- )} - {themeSettingKey === "fontFamily" && (
{name}
)} + + {themeSettingKey === "gridColumns" && ( +
+ +
+
+ + setGridColumns(value.toString())} + onChangeComplete={(value) => gridSizeInputBlur(value.toString())} + /> +
+ )} + + {themeSettingKey === "gridRowHeight" && ( +
+ +
+
+ + setGridRowHeight(value.toString())} + onChangeComplete={(value) => gridSizeInputBlur(value.toString())} + /> +
+ )} + + {themeSettingKey === "gridRowCount" && ( +
+ +
+
+ + { + if (e.target.value === '') { + return setGridRowCount(Infinity); + } + setGridRowCount(Number(e.target.value)) + }} + onBlur={(e) => gridRowCountInputBlur(e.target.value)} + onKeyUp={(e) => + e.nativeEvent.key === "Enter" && + gridRowCountInputBlur(e.currentTarget.value) + } + /> +
+ )} + + {themeSettingKey === "gridPaddingX" && ( +
+ +
+
+ { + if (e.target.value === '') { + return setGridPaddingX(undefined); + } + setGridPaddingX(Number(e.target.value)) + }} + onBlur={(e) => gridPaddingInputBlur(e.target.value)} + onKeyUp={(e) => + e.nativeEvent.key === "Enter" && + gridPaddingInputBlur(e.currentTarget.value) + } + /> +
+ )} + + {themeSettingKey === "gridPaddingY" && ( +
+ +
+
+ { + if (e.target.value === '') { + return setGridPaddingY(undefined); + } + setGridPaddingY(Number(e.target.value)) + }} + onBlur={(e) => gridPaddingInputBlur(e.target.value)} + onKeyUp={(e) => + e.nativeEvent.key === "Enter" && + gridPaddingInputBlur(e.currentTarget.value) + } + /> +
+ )} + + {themeSettingKey === "gridBgImage" && ( +
+ setGridBgImage(e.target.value)} + onBlur={(e) => gridBackgroundInputBlur(e.target.value)} + onKeyUp={(e) => e.nativeEvent.key === "Enter" && gridBackgroundInputBlur(e.currentTarget.value)} + /> +
+ )} + + {themeSettingKey === "gridBgImageRepeat" && ( +
+ setGridBgImageRepeat(e.target.value)} + onBlur={(e) => gridBackgroundInputBlur(e.target.value)} + onKeyUp={(e) => e.nativeEvent.key === "Enter" && gridBackgroundInputBlur(e.currentTarget.value)} + /> +
+ )} + + {themeSettingKey === "gridBgImageSize" && ( +
+ setGridBgImageSize(e.target.value)} + onBlur={(e) => gridBackgroundInputBlur(e.target.value)} + onKeyUp={(e) => e.nativeEvent.key === "Enter" && gridBackgroundInputBlur(e.currentTarget.value)} + /> +
+ )} + + {themeSettingKey === "gridBgImagePosition" && ( +
+ setGridBgImagePosition(e.target.value)} + onBlur={(e) => gridBackgroundInputBlur(e.target.value)} + onKeyUp={(e) => e.nativeEvent.key === "Enter" && gridBackgroundInputBlur(e.currentTarget.value)} + /> +
+ )} + + {themeSettingKey === "gridBgImageOrigin" && ( +
+ setGridBgImageOrigin(e.target.value)} + onBlur={(e) => gridBackgroundInputBlur(e.target.value)} + onKeyUp={(e) => e.nativeEvent.key === "Enter" && gridBackgroundInputBlur(e.currentTarget.value)} + /> +
+ )} ); } diff --git a/client/packages/lowcoder/src/components/table/EditableCell.tsx b/client/packages/lowcoder/src/components/table/EditableCell.tsx index f8f9ebe16..9fae29b50 100644 --- a/client/packages/lowcoder/src/components/table/EditableCell.tsx +++ b/client/packages/lowcoder/src/components/table/EditableCell.tsx @@ -1,7 +1,6 @@ import { PresetStatusColorType } from "antd/es/_util/colors"; import _ from "lodash"; import { changeChildAction, DispatchType } from "lowcoder-core"; -import { constantColors } from "lowcoder-design/src/components/colorSelect/colorUtils"; import React, { ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import { JSONValue } from "util/jsonTypes"; @@ -26,8 +25,8 @@ const EditableChip = styled.div` height: 0px; border: 4.5px solid transparent; border-radius: 2px; - border-top-color: ${constantColors[1].color}; - border-right-color: ${constantColors[1].color}; + border-top-color: #5589F2; + border-right-color: #5589F2; `; export interface CellProps { diff --git a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx index bec7edb67..05e8eed96 100644 --- a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx @@ -2,9 +2,9 @@ import { ThemeDetail, ThemeType } from "api/commonSettingApi"; import { RecordConstructorToComp } from "lowcoder-core"; import { dropdownInputSimpleControl } from "comps/controls/dropdownInputSimpleControl"; import { MultiCompBuilder, valueComp, withDefault } from "comps/generators"; -import { AddIcon, Dropdown } from "lowcoder-design"; +import { AddIcon, BaseSection, Dropdown } from "lowcoder-design"; import { EllipsisSpan } from "pages/setting/theme/styledComponents"; -import { useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { useSelector } from "react-redux"; import { getDefaultTheme, getThemeList } from "redux/selectors/commonSettingSelectors"; import styled, { css } from "styled-components"; @@ -14,7 +14,7 @@ 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 { NumberControl, RangeControl, StringControl } from "comps/controls/codeControl"; import { IconControl } from "comps/controls/iconControl"; import { dropdownControl } from "comps/controls/dropdownControl"; import { ApplicationCategoriesEnum } from "constants/applicationConstants"; @@ -22,6 +22,9 @@ import { BoolControl } from "../controls/boolControl"; import { getNpmPackageMeta } from "../utils/remote"; import { getPromiseAfterDispatch } from "@lowcoder-ee/util/promiseUtils"; import type { AppState } from "@lowcoder-ee/redux/reducers"; +import { ColorControl } from "../controls/colorControl"; +import { DEFAULT_ROW_COUNT } from "@lowcoder-ee/layout/calculateUtils"; +import { AppSettingContext } from "../utils/appSettingContext"; const TITLE = trans("appSetting.title"); const USER_DEFINE = "__USER_DEFINE"; @@ -100,18 +103,41 @@ const SettingsStyled = styled.div` `; const DivStyled = styled.div` + margin: 0 16px; + > div { flex-wrap: wrap; margin-bottom: 12px; > div { width: 100%; + flex: 0 0 100%; display: block; + + .tooltipLabel { + width: 100%; + } } > div:first-child { margin-bottom: 6px; } + + > div:nth-child(2) { + > div { + width: 100%; + justify-content: flex-start; + > div:first-child { + flex: 0 0 24px; + } + > div:nth-child(2) { + flex: 1; + > div:nth-child(2) { + width: 100%; + } + } + } + } } // custom styles for icon selector @@ -183,34 +209,152 @@ const childrenMap = { icon: IconControl, category: dropdownControl(AppCategories, ApplicationCategoriesEnum.BUSINESS), showHeaderInPublic: withDefault(BoolControl, true), - maxWidth: dropdownInputSimpleControl(OPTIONS, USER_DEFINE, "1920"), themeId: valueComp(DEFAULT_THEMEID), preventAppStylesOverwriting: withDefault(BoolControl, true), customShortcuts: CustomShortcutsComp, disableCollision: valueComp(false), lowcoderCompVersion: withDefault(StringControl, 'latest'), + maxWidth: dropdownInputSimpleControl(OPTIONS, USER_DEFINE, "1920"), + gridColumns: RangeControl.closed(8, 48, 24), + gridRowHeight: RangeControl.closed(6, 20, 8), + gridRowCount: withDefault(NumberControl, DEFAULT_ROW_COUNT), + gridPaddingX: withDefault(NumberControl, 20), + gridPaddingY: withDefault(NumberControl, 20), + gridBg: ColorControl, + gridBgImage: StringControl, + gridBgImageRepeat: StringControl, + gridBgImageSize: StringControl, + gridBgImagePosition: StringControl, + gridBgImageOrigin: StringControl, }; type ChildrenInstance = RecordConstructorToComp & { themeList: ThemeType[]; defaultTheme: string; }; -function AppSettingsModal(props: ChildrenInstance) { +function AppGeneralSettingsModal(props: ChildrenInstance) { const lowcoderCompsMeta = useSelector((state: AppState) => state.npmPlugin.packageMeta['lowcoder-comps']); const [lowcoderCompVersions, setLowcoderCompVersions] = useState(['latest']); const { - themeList, - defaultTheme, - themeId, - maxWidth, title, description, icon, category, showHeaderInPublic, - preventAppStylesOverwriting, lowcoderCompVersion, } = props; + + useEffect(() => { + setLowcoderCompVersions([ + 'latest', + ...Object.keys(lowcoderCompsMeta?.versions || []).reverse() + ]) + }, [lowcoderCompsMeta]) + + return ( + <> + + + {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"), + })} +
+
+ {showHeaderInPublic.propertyView({ + label: trans("appSetting.showPublicHeader"), + })} +
+
+
+ + + ({label: version, value: version})) + } + label={'Current Version'} + placement="bottom" + onChange={async (value) => { + await getPromiseAfterDispatch( + lowcoderCompVersion.dispatch, + lowcoderCompVersion.changeValueAction(value), { + autoHandleAfterReduce: true, + } + ) + setTimeout(() => { + window.location.reload(); + }, 1000); + }} + /> + + + + + {props.customShortcuts.getPropertyView()} + + + + ); +} + +function AppCanvasSettingsModal(props: ChildrenInstance) { + const { + themeList, + defaultTheme, + themeId, + preventAppStylesOverwriting, + maxWidth, + gridColumns, + gridRowHeight, + gridRowCount, + gridPaddingX, + gridPaddingY, + gridBg, + gridBgImage, + gridBgImageRepeat, + gridBgImageSize, + gridBgImagePosition, + gridBgImageOrigin, + } = props; const THEME_OPTIONS = themeList?.map((theme) => ({ label: theme.name, @@ -230,14 +374,6 @@ function AppSettingsModal(props: ChildrenInstance) { themeId.dispatchChangeValueAction(themeWithDefault); } }, [themeWithDefault]); - - useEffect(() => { - setLowcoderCompVersions([ - 'latest', - ...Object.keys(lowcoderCompsMeta?.versions || []).reverse() - ]) - }, [lowcoderCompsMeta]) - const DropdownItem = (params: { value: string }) => { const themeItem = themeList.find((theme) => theme.id === params.value); @@ -252,104 +388,124 @@ function AppSettingsModal(props: ChildrenInstance) { ); }; + return ( - - {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"), + <> + + + } + preNode={() => ( + <> + window.open(THEME_SETTING)}> + + {trans("appSetting.themeCreate")} + + + + )} + allowClear + onChange={(value) => { + themeId.dispatchChangeValueAction( + value === defaultTheme ? DEFAULT_THEMEID : value || "" + ); + }} + /> +
+ {preventAppStylesOverwriting.propertyView({ + label: trans("prop.preventOverwriting"), + })} +
+
+
+ + + {maxWidth.propertyView({ + dropdownLabel: trans("appSetting.canvasMaxWidth"), + inputLabel: trans("appSetting.userDefinedMaxWidth"), + inputPlaceholder: trans("appSetting.inputUserDefinedPxValue"), + placement: "bottom", + min: 350, + lastNode: {trans("appSetting.maxWidthTip")}, })} -
-
- {showHeaderInPublic.propertyView({ - label: trans("appSetting.showPublicHeader"), + {gridColumns.propertyView({ + label: trans("appSetting.gridColumns"), + placeholder: '24', })} -
- {maxWidth.propertyView({ - dropdownLabel: trans("appSetting.canvasMaxWidth"), - inputLabel: trans("appSetting.userDefinedMaxWidth"), - inputPlaceholder: trans("appSetting.inputUserDefinedPxValue"), - placement: "bottom", - min: 350, - lastNode: {trans("appSetting.maxWidthTip")}, - })} - } - preNode={() => ( - <> - window.open(THEME_SETTING)}> - - {trans("appSetting.themeCreate")} - - - - )} - allowClear - onChange={(value) => { - themeId.dispatchChangeValueAction( - value === defaultTheme ? DEFAULT_THEMEID : value || "" - ); - }} - /> -
- {preventAppStylesOverwriting.propertyView({ - label: trans("prop.preventOverwriting"), + {gridRowHeight.propertyView({ + label: trans("appSetting.gridRowHeight"), + placeholder: '8', })} -
-
- - - ({label: version, value: version})) - } - label={'Lowcoder Comps Version'} - placement="bottom" - onChange={async (value) => { - await getPromiseAfterDispatch( - lowcoderCompVersion.dispatch, - lowcoderCompVersion.changeValueAction(value), { - autoHandleAfterReduce: true, - } - ) - setTimeout(() => { - window.location.reload(); - }, 1000); - }} - /> - - - {props.customShortcuts.getPropertyView()} -
+ {gridRowCount.propertyView({ + label: trans("appSetting.gridRowCount"), + placeholder: 'Infinity', + })} + {gridPaddingX.propertyView({ + label: trans("appSetting.gridPaddingX"), + placeholder: '20', + })} + {gridPaddingY.propertyView({ + label: trans("appSetting.gridPaddingY"), + placeholder: '20', + })} + {gridBg.propertyView({ + label: trans("style.background"), + allowGradient: true, + })} + {gridBgImage.propertyView({ + label: trans("appSetting.gridBgImage"), + placeholder: '', + })} + {gridBgImageRepeat.propertyView({ + label: trans("appSetting.gridBgImageRepeat"), + placeholder: 'no-repeat', + })} + {gridBgImageSize.propertyView({ + label: trans("appSetting.gridBgImageSize"), + placeholder: 'cover', + })} + {gridBgImagePosition.propertyView({ + label: trans("appSetting.gridBgImagePosition"), + placeholder: 'center', + })} + {gridBgImageOrigin.propertyView({ + label: trans("appSetting.gridBgImageOrigin"), + placeholder: 'no-padding', + })} + + + ); } + export const AppSettingsComp = new MultiCompBuilder(childrenMap, (props) => { return { ...props, @@ -357,8 +513,12 @@ export const AppSettingsComp = new MultiCompBuilder(childrenMap, (props) => { }; }) .setPropertyViewFn((children) => { + const { settingType } = useContext(AppSettingContext); const themeList = useSelector(getThemeList) || []; const defaultTheme = (useSelector(getDefaultTheme) || "").toString(); - return ; + + return settingType === 'canvas' + ? + : ; }) .build(); diff --git a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx index e97769184..4ab79a2ea 100644 --- a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx @@ -285,6 +285,7 @@ let AutoCompleteCompBase = (function () { labelStyle: props.labelStyle, inputFieldStyle:props.inputFieldStyle, animationStyle: props.animationStyle, + showValidationWhenEmpty: props.showValidationWhenEmpty, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/buttonCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/buttonCompConstants.tsx index e2517d306..f83b39939 100644 --- a/client/packages/lowcoder/src/comps/comps/buttonComp/buttonCompConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/buttonComp/buttonCompConstants.tsx @@ -27,7 +27,7 @@ export function getButtonStyle(buttonStyle: ButtonStyleType) { font-style: ${buttonStyle.fontStyle}; text-transform:${buttonStyle.textTransform}; text-decoration:${buttonStyle.textDecoration}; - background-color: ${buttonStyle.background}; + background: ${buttonStyle.background}; border-radius: ${buttonStyle.radius}; margin: ${buttonStyle.margin}; padding: ${buttonStyle.padding}; diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/dropdownComp.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/dropdownComp.tsx index 11e9bd402..1ce2b491e 100644 --- a/client/packages/lowcoder/src/comps/comps/buttonComp/dropdownComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/buttonComp/dropdownComp.tsx @@ -52,7 +52,7 @@ const LeftButtonWrapper = styled.div<{ $buttonStyle: DropdownStyleType }>` ${(props) => `text-transform: ${props.$buttonStyle.textTransform};`} ${(props) => `font-weight: ${props.$buttonStyle.textWeight};`} } - ${(props) => `background-color: ${props.$buttonStyle.background};`} + ${(props) => `background: ${props.$buttonStyle.background};`} ${(props) => `color: ${props.$buttonStyle.text};`} ${(props) => `padding: ${props.$buttonStyle.padding};`} ${(props) => `font-size: ${props.$buttonStyle.textSize};`} diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/floatButtonComp.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/floatButtonComp.tsx index 3ae02259b..223650ef4 100644 --- a/client/packages/lowcoder/src/comps/comps/buttonComp/floatButtonComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/buttonComp/floatButtonComp.tsx @@ -37,7 +37,7 @@ const Wrapper = styled.div<{ $badgeStyle: BadgeStyleType, $style: FloatButtonSty inset-block-end: -8px; } .ant-float-btn-primary .ant-float-btn-body { - background-color: ${(props) => props.$style.background}; + background: ${(props) => props.$style.background}; border: ${(props) => props.$style.border}; border-style: ${(props) => props.$style.borderStyle}; border-width: ${(props) => props.$style.borderWidth}; diff --git a/client/packages/lowcoder/src/comps/comps/buttonComp/linkComp.tsx b/client/packages/lowcoder/src/comps/comps/buttonComp/linkComp.tsx index a1f71169e..31ed59809 100644 --- a/client/packages/lowcoder/src/comps/comps/buttonComp/linkComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/buttonComp/linkComp.tsx @@ -41,7 +41,7 @@ const Link = styled(Button)<{ 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}; + background: ${props.$style.background}; &:hover { color: ${props.$style.hoverText} !important; } diff --git a/client/packages/lowcoder/src/comps/comps/carouselComp.tsx b/client/packages/lowcoder/src/comps/comps/carouselComp.tsx index 3db1ccfb7..e4b5f26d0 100644 --- a/client/packages/lowcoder/src/comps/comps/carouselComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/carouselComp.tsx @@ -29,7 +29,7 @@ const Container = styled.div<{$bg: string; $animationStyle:AnimationStyleType}>` &, .ant-carousel { height: 100%; - background-color: ${(props) => props.$bg}; + background: ${(props) => props.$bg}; ${props=>props.$animationStyle} } `; diff --git a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx index df3fe5011..fa602fa6b 100644 --- a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx @@ -41,6 +41,7 @@ import { EditorContext } from "comps/editorState"; import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils"; import { DisabledContext } from "comps/generators/uiCompBuilder"; import SliderControl from "@lowcoder-ee/comps/controls/sliderControl"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const ContainWrapper = styled.div<{ $style: ContainerStyleType & { @@ -57,13 +58,13 @@ const ContainWrapper = styled.div<{ column-gap: ${(props) => props.$style?.columnGap}; row-gap: ${(props) => props.$style?.rowGap}; - background-color: ${(props) => props.$style?.background} !important; border-radius: ${(props) => props.$style?.radius}; border-width: ${(props) => props.$style?.borderWidth}; border-color: ${(props) => props.$style?.border}; border-style: ${(props) => props.$style?.borderStyle}; margin: ${(props) => props.$style?.margin}; padding: ${(props) => props.$style?.padding}; + ${props => props.$style && getBackgroundStyle(props.$style)} `; const ColWrapper = styled(Col)<{ @@ -73,13 +74,13 @@ const ColWrapper = styled(Col)<{ }>` > div { height: ${(props) => props.$matchColumnsHeight ? `calc(100% - ${props.$style?.padding || 0} - ${props.$style?.padding || 0})` : 'auto'}; - background-color: ${(props) => props.$style?.background} !important; border-radius: ${(props) => props.$style?.radius}; border-width: ${(props) => props.$style?.borderWidth}; border-color: ${(props) => props.$style?.border}; border-style: ${(props) => props.$style?.borderStyle}; margin: ${(props) => props.$style?.margin}; padding: ${(props) => props.$style?.padding}; + ${props => props.$style && getBackgroundStyle(props.$style)} } `; diff --git a/client/packages/lowcoder/src/comps/comps/containerComp/cardComp.tsx b/client/packages/lowcoder/src/comps/comps/containerComp/cardComp.tsx index 9e03ccb36..f9a14575f 100644 --- a/client/packages/lowcoder/src/comps/comps/containerComp/cardComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/containerComp/cardComp.tsx @@ -20,6 +20,7 @@ import { ButtonEventHandlerControl, CardEventHandlerControl, clickEvent, refresh import { optionsControl } from "comps/controls/optionsControl"; import { dropdownControl } from "comps/controls/dropdownControl"; import { styleControl } from "comps/controls/styleControl"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const { Meta } = Card; @@ -34,7 +35,6 @@ const Wrapper = styled.div<{ height: 100%; width: 100%; .ant-card-small >.ant-card-head { - background-color: ${props => props.$headerStyle?.background} !important; border: ${props => props.$headerStyle?.border}; border-style: ${props => props.$headerStyle?.borderStyle}; border-width: ${props => props.$headerStyle?.borderWidth}; @@ -49,6 +49,7 @@ const Wrapper = styled.div<{ rotate: ${props => props.$headerStyle?.rotation}; margin: ${props => props.$headerStyle?.margin}; padding: ${props => props.$headerStyle?.padding}; + ${props => getBackgroundStyle(props.$headerStyle)} } .ant-card-head-title{ font-size: ${props => props.$headerStyle?.textSize}; @@ -61,10 +62,9 @@ const Wrapper = styled.div<{ border-inline-end: 1px solid ${props => props.$style?.border}; } .ant-card .ant-card-actions { - background-color: ${props => props.$style?.background}; + ${props => props.$style && getBackgroundStyle(props.$style)} } .ant-card .ant-card-body { - background-color: ${props => props.$bodyStyle?.background} !important; border: ${props => props.$bodyStyle?.border}; border-style: ${props => props.$bodyStyle?.borderStyle}; border-width: ${props => props.$bodyStyle?.borderWidth}; @@ -72,6 +72,7 @@ const Wrapper = styled.div<{ rotate: ${props => props.$bodyStyle?.rotation}; margin: ${props => props.$bodyStyle?.margin}; padding: ${props => props.$bodyStyle?.padding}; + ${props => getBackgroundStyle(props.$bodyStyle)} } .ant-card { display: flex; @@ -79,13 +80,13 @@ const Wrapper = styled.div<{ justify-content: space-between; margin: ${props => props.$style?.margin}; padding: ${props => props.$style?.padding}; - background-color: ${props => props.$style?.background}; border: ${props => props.$style?.border}; rotate: ${props => props.$style?.rotation}; border-style: ${props => props.$style?.borderStyle}; border-radius: ${props => props.$style?.radius}; border-width: ${props => props.$style?.borderWidth}; box-shadow: ${props=>`${props.$style?.boxShadow} ${props.$style?.boxShadowColor}`}; + ${props => props.$style && getBackgroundStyle(props.$style)} ${props=>props.$animationStyle} } .ant-card-body { diff --git a/client/packages/lowcoder/src/comps/comps/containerComp/containerView.tsx b/client/packages/lowcoder/src/comps/comps/containerComp/containerView.tsx index ce9ddac21..1e47c5703 100644 --- a/client/packages/lowcoder/src/comps/comps/containerComp/containerView.tsx +++ b/client/packages/lowcoder/src/comps/comps/containerComp/containerView.tsx @@ -22,6 +22,7 @@ import { calcRowCount, calcRowHeight, DEFAULT_GRID_COLUMNS, + DEFAULT_ROW_COUNT, DEFAULT_ROW_HEIGHT, } from "layout/calculateUtils"; import _, { isEqual } from "lodash"; @@ -342,20 +343,20 @@ export const InnerGrid = React.memo((props: ViewPropsWithSelect) => { const horizontalGridCells = props.horizontalGridCells ? String(props.horizontalGridCells) : undefined; const currentTheme = useContext(ThemeContext)?.theme; const [currentRowCount, setRowCount] = useState(rowCount || Infinity); - const [currentRowHeight, setRowHeight] = useState(DEFAULT_ROW_HEIGHT); + const [currentRowHeight, setRowHeight] = useState(positionParams.rowHeight || DEFAULT_ROW_HEIGHT); const editorState = useContext(EditorContext); const { readOnly } = useContext(ExternalEditorContext); + const appSettingsComp = editorState.getAppSettingsComp().getView(); + + const maxWidth = useMemo(() => appSettingsComp.maxWidth, [appSettingsComp.maxWidth]); // Falk: TODO: Here we can define the inner grid columns dynamically - //Added By Aqib Mirza const defaultGrid = useMemo(() => { - return horizontalGridCells || - currentTheme?.gridColumns || - defaultTheme?.gridColumns || - "12"; - }, [horizontalGridCells, currentTheme?.gridColumns, defaultTheme?.gridColumns]); + return horizontalGridCells + || String(positionParams.cols) + || String(DEFAULT_GRID_COLUMNS); + }, [horizontalGridCells, positionParams.cols]); - ///////////////////// const isDroppable = useContext(IsDroppable) && (_.isNil(props.isDroppable) || props.isDroppable) && !readOnly; const isDraggable = !readOnly && (_.isNil(props.isDraggable) || props.isDraggable); @@ -479,14 +480,12 @@ export const InnerGrid = React.memo((props: ViewPropsWithSelect) => { useEffect(() => { if (!isRowCountLocked) { - setRowHeight(DEFAULT_ROW_HEIGHT); + setRowHeight(positionParams.rowHeight || DEFAULT_ROW_HEIGHT); setRowCount(Infinity); onRowCountChange?.(0); } }, [isRowCountLocked, onRowCountChange]); - const maxWidth = editorState.getAppSettings().maxWidth; - // log.info("rowCount:", currentRowCount, "rowHeight:", currentRowHeight); return ( @@ -543,6 +542,7 @@ export const InnerGrid = React.memo((props: ViewPropsWithSelect) => { onResizeStop={() => editorState.setDragging(false)} margin={[0, 0]} containerPadding={props.containerPadding} + fixedRowCount={props.emptyRows !== DEFAULT_ROW_COUNT} emptyRows={props.emptyRows} maxRows={currentRowCount} rowHeight={currentRowHeight} diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx index 89ff6a611..62f84bf84 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx @@ -56,6 +56,7 @@ import { timeZoneOptions } from "./timeZone"; const EventOptions = [changeEvent, focusEvent, blurEvent] as const; const validationChildren = { + showValidationWhenEmpty: BoolControl, required: BoolControl, minDate: StringControl, maxDate: StringControl, @@ -224,6 +225,7 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon} /> ), + showValidationWhenEmpty: props.showValidationWhenEmpty, ...validate(props), }); }) @@ -247,6 +249,9 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
{requiredPropertyView(children)} + {children.showValidationWhenEmpty.propertyView({ + label: trans("prop.showEmptyValidation") + })} {dateValidationFields(children)} {timeValidationFields(children)} {children.customRule.propertyView({})} @@ -381,6 +386,7 @@ export const dateRangeControl = (function () { children: children, inputFieldStyle:props.inputFieldStyle, onMouseDown: (e) => e.stopPropagation(), + showValidationWhenEmpty: props.showValidationWhenEmpty, ...(startResult.validateStatus !== "success" ? startResult : endResult.validateStatus !== "success" @@ -413,6 +419,9 @@ export const dateRangeControl = (function () { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
{requiredPropertyView(children)} + {children.showValidationWhenEmpty.propertyView({ + label: trans("prop.showEmptyValidation") + })} {dateValidationFields(children)} {timeValidationFields(children)} {children.customRule.propertyView({})} diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx index e34ae15a8..4d9f0a6e5 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx @@ -59,6 +59,7 @@ import { timeZoneOptions } from "./timeZone"; const EventOptions = [changeEvent, focusEvent, blurEvent] as const; const validationChildren = { + showValidationWhenEmpty: BoolControl, required: BoolControl, minTime: StringControl, maxTime: StringControl, @@ -116,7 +117,7 @@ function validate( } const current = dayjs(props.value.value, TimeParser); - if (props.required && !current.isValid()) { + if (props.required && (!Boolean(props.value.value) || !current.isValid())) { return { validateStatus: "error", help: trans("prop.required") }; } return { validateStatus: "success" }; @@ -197,6 +198,7 @@ export const timePickerControl = new UICompBuilder(childrenMap, (props) => { onBlur={() => props.onEvent("blur")} suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon} /> ), + showValidationWhenEmpty: props.showValidationWhenEmpty, ...validate(props), }); }) @@ -217,6 +219,9 @@ export const timePickerControl = new UICompBuilder(childrenMap, (props) => { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
{requiredPropertyView(children)} + {children.showValidationWhenEmpty.propertyView({ + label: trans("prop.showEmptyValidation") + })} {minTimePropertyView(children)} {maxTimePropertyView(children)} {children.customRule.propertyView({})} @@ -338,6 +343,7 @@ export const timeRangeControl = (function () { animationStyle:props.animationStyle, children: children, onMouseDown: (e) => e.stopPropagation(), + showValidationWhenEmpty: props.showValidationWhenEmpty, ...(startResult.validateStatus !== "success" ? startResult : endResult.validateStatus !== "success" @@ -366,6 +372,9 @@ export const timeRangeControl = (function () { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
{requiredPropertyView(children)} + {children.showValidationWhenEmpty.propertyView({ + label: trans("prop.showEmptyValidation") + })} {minTimePropertyView(children)} {maxTimePropertyView(children)} {children.customRule.propertyView({})} diff --git a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx index 908721570..5ce11399e 100644 --- a/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileComp/fileComp.tsx @@ -156,7 +156,7 @@ const getStyle = (style: FileStyleType) => { .ant-btn:not(:disabled) { border-color: ${style.border}; - background-color: ${style.background}; + background: ${style.background}; color: ${style.text}; &:hover, diff --git a/client/packages/lowcoder/src/comps/comps/fileViewerComp.tsx b/client/packages/lowcoder/src/comps/comps/fileViewerComp.tsx index 4b53dea3a..1c553bca9 100644 --- a/client/packages/lowcoder/src/comps/comps/fileViewerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/fileViewerComp.tsx @@ -22,7 +22,7 @@ const getStyle = (style: FileViewerStyleType) => { padding: ${style.padding}; overflow: hidden; - background-color: ${style.background}; + background: ${style.background}; border: ${(props) => (style.borderWidth ? style.borderWidth : "1px")} solid ${style.border}; border-radius: calc(min(${style.radius}, 20px)); `; diff --git a/client/packages/lowcoder/src/comps/comps/gridLayoutComp/canvasView.tsx b/client/packages/lowcoder/src/comps/comps/gridLayoutComp/canvasView.tsx index cc655730d..4b0f769aa 100644 --- a/client/packages/lowcoder/src/comps/comps/gridLayoutComp/canvasView.tsx +++ b/client/packages/lowcoder/src/comps/comps/gridLayoutComp/canvasView.tsx @@ -1,6 +1,6 @@ import { EditorContext } from "comps/editorState"; import { EditorContainer } from "pages/common/styledComponent"; -import React, { Profiler, useContext, useRef, useState } from "react"; +import React, { Profiler, useContext, useMemo, useRef, useState } from "react"; import styled from "styled-components"; import { profilerCallback } from "util/cacheUtils"; import { @@ -18,17 +18,36 @@ import { ThemeContext } from "comps/utils/themeContext"; import { checkIsMobile } from "util/commonUtils"; import { CanvasContainerID } from "constants/domLocators"; import { CNRootContainer } from "constants/styleSelectors"; -import { ScrollBar } from "lowcoder-design"; +import { isValidColor, isValidGradient, ScrollBar } from "lowcoder-design"; import { defaultTheme } from "@lowcoder-ee/constants/themeConstants"; import { isEqual } from "lodash"; +import { DEFAULT_GRID_COLUMNS, DEFAULT_ROW_COUNT, DEFAULT_ROW_HEIGHT } from "@lowcoder-ee/layout/calculateUtils"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; -// min-height: 100vh; - -const UICompContainer = styled.div<{ $maxWidth?: number; readOnly?: boolean; $bgColor: string }>` - height: 100%; +const UICompContainer = styled.div<{ + $maxWidth?: number; + $rowCount?: number; + readOnly?: boolean; + $bgColor: string; + $bgImage?: string; + $bgImageSize?: string; + $bgImageRepeat?: string; + $bgImageOrigin?: string; + $bgImagePosition?: string; +}>` + height: auto; + min-height: ${props => props.$rowCount === Infinity ? '100%' : 'auto'}; margin: 0 auto; max-width: ${(props) => props.$maxWidth || 1600}px; - background-color: ${(props) => props.$bgColor}; + + ${props => getBackgroundStyle({ + background: props.$bgColor, + backgroundImage: props.$bgImage, + backgroundImageSize: props.$bgImageSize, + backgroundImageRepeat: props.$bgImageRepeat, + backgroundImageOrigin: props.$bgImageOrigin, + backgroundImagePosition: props.$bgImagePosition, + })} `; // modal/drawer container @@ -74,55 +93,183 @@ function getDragSelectedNames( const EmptySet = new Set(); export const CanvasView = React.memo((props: ContainerBaseProps) => { + const currentTheme = useContext(ThemeContext)?.theme; + const isDefaultTheme = useContext(ThemeContext)?.themeId === 'default-theme-id'; + const isPreviewTheme = useContext(ThemeContext)?.themeId === 'preview-theme'; const editorState = useContext(EditorContext); const [dragSelectedComps, setDragSelectedComp] = useState(EmptySet); const scrollContainerRef = useRef(null); - + const appSettings = editorState.getAppSettings(); const maxWidthFromHook = useMaxWidth(); - const maxWidth = editorState.getAppSettings().maxWidth ?? maxWidthFromHook; + + const maxWidth = useMemo( + () => appSettings.maxWidth ?? maxWidthFromHook, + [appSettings, maxWidthFromHook] + ); + + const preventStylesOverwriting = useMemo( + () => appSettings.preventAppStylesOverwriting, + [appSettings] + ); + const isMobile = checkIsMobile(maxWidth); - const defaultContainerPadding = isMobile ? DEFAULT_MOBILE_PADDING : DEFAULT_CONTAINER_PADDING; + // const defaultContainerPadding = isMobile ? DEFAULT_MOBILE_PADDING : DEFAULT_CONTAINER_PADDING; const externalState = useContext(ExternalEditorContext); const { readOnly, appType, rootContainerExtraHeight = DEFAULT_EXTRA_HEIGHT, - rootContainerPadding = defaultContainerPadding, + rootContainerPadding, rootContainerOverflow, } = externalState; const isModule = appType === AppTypeEnum.Module; - const bgColor = (useContext(ThemeContext)?.theme || defaultTheme).canvas; - // Added By Aqib Mirza - const defaultGrid = - useContext(ThemeContext)?.theme?.gridColumns || - defaultTheme?.gridColumns || - "24"; + const bgColor = useMemo( + () => { + if (isPreviewTheme) return currentTheme?.canvas ?? defaultTheme.canvas; + + const themeGridBgColor = preventStylesOverwriting ? undefined : currentTheme?.canvas; + return themeGridBgColor || appSettings.gridBg || defaultTheme.canvas; + }, + [preventStylesOverwriting, appSettings, currentTheme, defaultTheme] + ); + + const bgImage = useMemo( + () => { + if (isPreviewTheme) return currentTheme?.gridBgImage; + + const themeGridBgImage = preventStylesOverwriting ? undefined : currentTheme?.gridBgImage; + return themeGridBgImage || appSettings.gridBgImage; + }, + [preventStylesOverwriting, appSettings, currentTheme], + ); + + const bgImageRepeat = useMemo( + () => { + if (isPreviewTheme) return currentTheme?.gridBgImageRepeat ?? defaultTheme.gridBgImageRepeat; + + const themeGridBgImageRepeat = preventStylesOverwriting ? undefined : currentTheme?.gridBgImageRepeat; + return themeGridBgImageRepeat || appSettings.gridBgImageRepeat || defaultTheme?.gridBgImageRepeat; + }, + [preventStylesOverwriting, appSettings, currentTheme, defaultTheme], + ); + const bgImageSize = useMemo( + () => { + if (isPreviewTheme) return currentTheme?.gridBgImageSize ?? defaultTheme.gridBgImageSize; + + const themeGridBgImageSize = preventStylesOverwriting ? undefined : currentTheme?.gridBgImageSize; + return themeGridBgImageSize || appSettings.gridBgImageSize || defaultTheme?.gridBgImageSize; + }, + [preventStylesOverwriting, appSettings, currentTheme, defaultTheme], + ); + const bgImagePosition = useMemo( + () => { + if (isPreviewTheme) return currentTheme?.gridBgImagePosition ?? defaultTheme.gridBgImagePosition; + + const themeGridBgImagePosition = preventStylesOverwriting ? undefined : currentTheme?.gridBgImagePosition; + return themeGridBgImagePosition || appSettings.gridBgImagePosition || defaultTheme?.gridBgImagePosition; + }, + [preventStylesOverwriting, appSettings, currentTheme, defaultTheme], + ); + const bgImageOrigin = useMemo( + () => { + if (isPreviewTheme) return currentTheme?.gridBgImageOrigin ?? defaultTheme.gridBgImageOrigin; + + const themeGridBgImageOrigin = preventStylesOverwriting ? undefined : currentTheme?.gridBgImageOrigin; + return themeGridBgImageOrigin || appSettings.gridBgImageOrigin || defaultTheme?.gridBgImageOrigin; + }, + [preventStylesOverwriting, appSettings, currentTheme, defaultTheme], + ); + + const defaultGrid = useMemo(() => { + if (isPreviewTheme) return currentTheme?.gridColumns ?? defaultTheme.gridColumns ?? String(DEFAULT_GRID_COLUMNS); + + const themeGridColumns = preventStylesOverwriting ? undefined : currentTheme?.gridColumns; + return themeGridColumns + || String(appSettings?.gridColumns) + || defaultTheme?.gridColumns + || String(DEFAULT_GRID_COLUMNS); + }, [preventStylesOverwriting, appSettings, currentTheme, defaultTheme]); + + const defaultRowHeight = useMemo(() => { + if (isPreviewTheme) return currentTheme?.gridRowHeight ?? defaultTheme.gridRowHeight ?? String(DEFAULT_ROW_HEIGHT); + + const themeGridRowHeight = preventStylesOverwriting ? undefined : currentTheme?.gridRowHeight; + return themeGridRowHeight + || String(appSettings?.gridRowHeight) + || defaultTheme?.gridRowHeight + || String(DEFAULT_ROW_HEIGHT); + }, [preventStylesOverwriting, appSettings, currentTheme, defaultTheme]); - const positionParams = { + const defaultRowCount = useMemo(() => { + if (isPreviewTheme) return currentTheme?.gridRowCount ?? defaultTheme.gridRowCount; + + const themeGridRowCount = preventStylesOverwriting ? undefined : currentTheme?.gridRowCount; + return themeGridRowCount + || appSettings?.gridRowCount + || defaultTheme?.gridRowCount + || DEFAULT_ROW_COUNT; + }, [preventStylesOverwriting, appSettings, currentTheme, defaultTheme]); + + const defaultContainerPadding: [number, number] = useMemo(() => { + const DEFAULT_PADDING = isMobile ? DEFAULT_MOBILE_PADDING : DEFAULT_CONTAINER_PADDING; + + if (isPreviewTheme) { + return [ + currentTheme?.gridPaddingX ?? defaultTheme.gridPaddingX ?? DEFAULT_PADDING[0], + currentTheme?.gridPaddingY ?? defaultTheme.gridPaddingY ?? DEFAULT_PADDING[1], + ]; + } + + const themeGridPaddingX = preventStylesOverwriting || isDefaultTheme ? undefined : currentTheme?.gridPaddingX; + const themeGridPaddingY = preventStylesOverwriting || isDefaultTheme ? undefined : currentTheme?.gridPaddingY; + + let paddingX = themeGridPaddingX ?? appSettings?.gridPaddingX ?? defaultTheme?.gridPaddingX ?? DEFAULT_PADDING[0]; + let paddingY = themeGridPaddingY ?? appSettings?.gridPaddingY ?? defaultTheme?.gridPaddingY ?? DEFAULT_PADDING[1]; + + return [paddingX, paddingY]; + }, [preventStylesOverwriting, appSettings, isMobile, currentTheme, defaultTheme]); + + const defaultMinHeight = useMemo(() => { + return defaultRowCount === DEFAULT_ROW_COUNT + ? `calc(100vh - ${TopHeaderHeight} - ${EditorContainerPadding} * 2)` + : undefined; + }, [defaultRowCount]); + + const positionParams = useMemo(() => ({ ...props.positionParams, cols: parseInt(defaultGrid), - }; - ////////////////////// + rowHeight: parseInt(defaultRowHeight), + }), [props.positionParams, defaultGrid, defaultRowHeight]); + if (readOnly) { return ( @@ -132,7 +279,17 @@ export const CanvasView = React.memo((props: ContainerBaseProps) => { return ( - + { setDragSelectedComp(EmptySet); @@ -148,18 +305,20 @@ export const CanvasView = React.memo((props: ContainerBaseProps) => { > diff --git a/client/packages/lowcoder/src/comps/comps/iconComp.tsx b/client/packages/lowcoder/src/comps/comps/iconComp.tsx index ef1076b32..990f8531c 100644 --- a/client/packages/lowcoder/src/comps/comps/iconComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/iconComp.tsx @@ -37,7 +37,7 @@ const Container = styled.div<{ display: flex; align-items: center; justify-content: center; -${props=>props.$animationStyle} + ${props=>props.$animationStyle} ${(props) => props.$style && css` diff --git a/client/packages/lowcoder/src/comps/comps/iframeComp.tsx b/client/packages/lowcoder/src/comps/comps/iframeComp.tsx index 955ceb1e3..889444883 100644 --- a/client/packages/lowcoder/src/comps/comps/iframeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/iframeComp.tsx @@ -30,7 +30,7 @@ ${props=>props.$animationStyle} width: 100%; height: 100%; display: block; - background-color: ${(props) => props.$style.background}; + background: ${(props) => props.$style.background}; } `; diff --git a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx index 34d7ad607..89686dbe5 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonComp/jsonLottieComp.tsx @@ -115,7 +115,7 @@ let JsonLottieTmpComp = (function () { height: "100%", display: "flex", justifyContent: "center", - backgroundColor: `${props.container.background}`, + background: `${props.container.background}`, padding: `${props.container.padding}`, rotate: props.container.rotation, }} diff --git a/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx b/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx index 5390db39b..71503d0c2 100644 --- a/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx +++ b/client/packages/lowcoder/src/comps/comps/listViewComp/listView.tsx @@ -21,6 +21,7 @@ import { getCurrentItemParams, getData } from "./listViewUtils"; import { useMergeCompStyles } from "@lowcoder-ee/util/hooks"; import { childrenToProps } from "@lowcoder-ee/comps/generators/multi"; import { AnimationStyleType } from "@lowcoder-ee/comps/controls/styleControlConstants"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const ListViewWrapper = styled.div<{ $style: any; $paddingWidth: string,$animationStyle:AnimationStyleType }>` height: 100%; @@ -28,7 +29,7 @@ const ListViewWrapper = styled.div<{ $style: any; $paddingWidth: string,$animati border-radius: ${(props) => props.$style.radius}; padding: 3px ${(props) => props.$paddingWidth}; rotate: ${(props) => props.$style.rotation}; - background-color: ${(props) => props.$style.background}; + ${props => getBackgroundStyle(props.$style)} ${props=>props.$animationStyle} `; diff --git a/client/packages/lowcoder/src/comps/comps/meetingComp/videobuttonCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/meetingComp/videobuttonCompConstants.tsx index 2b74fd189..6bdf5c242 100644 --- a/client/packages/lowcoder/src/comps/comps/meetingComp/videobuttonCompConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/meetingComp/videobuttonCompConstants.tsx @@ -27,7 +27,7 @@ export function getButtonStyle(buttonStyle: any) { --antd-wave-shadow-color: ${buttonStyle.border}; border-color: ${buttonStyle.border}; color: ${buttonStyle.text}; - background-color: ${buttonStyle.background}; + background: ${buttonStyle.background}; border-radius: ${buttonStyle.radius}; margin: ${buttonStyle.margin}; padding: ${buttonStyle.padding}; diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx index 1f9e10dea..75b483072 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx @@ -41,7 +41,7 @@ ${props=>props.$animationStyle} border-radius: ${(props) =>props.$borderRadius ? props.$borderRadius : '2px'}; box-sizing: border-box; border: ${(props) => props.$borderWidth ? `${props.$borderWidth}` : '1px'} ${props=>props.$borderStyle} ${(props) => props.$borderColor}; - background-color: ${(props) => props.$bgColor}; + background: ${(props) => props.$bgColor}; `; const NavInner = styled("div") >` diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx index 358235e56..4f8ad6bce 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx @@ -268,6 +268,7 @@ const childrenMap = { inputFieldStyle: styleControl(InputLikeStyle , 'inputFieldStyle'), // validation required: BoolControl, + showValidationWhenEmpty: BoolControl, min: UndefinedNumberControl, max: UndefinedNumberControl, customRule: CustomRuleControl, @@ -389,6 +390,7 @@ let NumberInputTmpComp = (function () { labelStyle: props.labelStyle, inputFieldStyle:props.inputFieldStyle, animationStyle:props.animationStyle, + showValidationWhenEmpty: props.showValidationWhenEmpty, ...validate(props), }); }) @@ -405,6 +407,7 @@ let NumberInputTmpComp = (function () { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
{requiredPropertyView(children)} + {children.showValidationWhenEmpty.propertyView({label: trans("prop.showEmptyValidation")})} {children.min.propertyView({ label: trans("prop.minimum") })} {children.max.propertyView({ label: trans("prop.maximum") })} {children.customRule.propertyView({})} diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx index cac189864..0c17a5a7f 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx @@ -22,7 +22,7 @@ const RangeSliderBasicComp = (function () { animationStyle:props.animationStyle, children: ( { e.stopPropagation(); return false; @@ -35,7 +35,7 @@ const RangeSliderBasicComp = (function () { value={[props.start.value, props.end.value]} $style={props.inputFieldStyle} style={{ margin: 0 }} - vertical={props.vertical || false} + $vertical={Boolean(props.vertical) || false} onChange={([start, end]) => { props.start.onChange(start); props.end.onChange(end); diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx index a73b39a47..cabda6634 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx @@ -26,7 +26,7 @@ const SliderBasicComp = (function () { animationStyle:props.animationStyle, children: ( { e.stopPropagation(); return false; @@ -38,7 +38,7 @@ const SliderBasicComp = (function () { value={props.value.value} $style={props.inputFieldStyle} style={{margin: 0}} - vertical={props.vertical || false} + $vertical={Boolean(props.vertical) || false} onChange={(e) => { props.value.onChange(e); props.onEvent("change"); diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx index 771d0d73c..efee00caa 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx @@ -60,11 +60,11 @@ const getStyle = (style: SliderStyleType, vertical: boolean) => { `; }; -export const SliderStyled = styled(Slider)<{ $style: SliderStyleType, vertical: boolean }>` - ${(props) => props.$style && getStyle(props.$style, props.vertical)} +export const SliderStyled = styled(Slider)<{ $style: SliderStyleType, $vertical: boolean }>` + ${(props) => props.$style && getStyle(props.$style, props.$vertical)} `; -export const SliderWrapper = styled.div<{ vertical: boolean }>` +export const SliderWrapper = styled.div<{ $vertical: boolean }>` width: 100%; display: inline-flex; align-items: center; diff --git a/client/packages/lowcoder/src/comps/comps/pageLayoutComp/pageLayout.tsx b/client/packages/lowcoder/src/comps/comps/pageLayoutComp/pageLayout.tsx index 105164b0d..bfece6cf2 100644 --- a/client/packages/lowcoder/src/comps/comps/pageLayoutComp/pageLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/pageLayoutComp/pageLayout.tsx @@ -11,6 +11,7 @@ import { ConfigProvider, Layout } from 'antd'; import { contrastBackground, contrastText } from "comps/controls/styleControlConstants"; import { useRef, useState } from "react"; import { LowcoderAppView } from "appView/LowcoderAppView"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const { Header, Content, Footer, Sider } = Layout; @@ -30,12 +31,7 @@ const getStyle = (style: ContainerStyleType) => { border-radius: ${style.radius}; overflow: hidden; padding: ${style.padding}; - ${style.background && `background-color: ${style.background};`} - ${style.backgroundImage && `background-image: url(${style.backgroundImage});`} - ${style.backgroundImageRepeat && `background-repeat: ${style.backgroundImageRepeat};`} - ${style.backgroundImageSize && `background-size: ${style.backgroundImageSize};`} - ${style.backgroundImagePosition && `background-position: ${style.backgroundImagePosition};`} - ${style.backgroundImageOrigin && `background-origin: ${style.backgroundImageOrigin};`} + ${style && getBackgroundStyle(style)} `; }; @@ -51,14 +47,30 @@ const Wrapper = styled.div<{ $style: ContainerStyleType,$animationStyle:Animatio #pageLayout::-webkit-scrollbar { display: ${(props) => props.$mainScrollbars ? "block" : "none"}; } + + .ant-layout { + background: transparent; + } `; const HeaderInnerGrid = styled(InnerGrid)<{ - $backgroundColor: string + $backgroundColor: string, + $headerBackgroundImage: string, + $headerBackgroundImageSize: string, + $headerBackgroundImageRepeat: string, + $headerBackgroundImageOrigin: string, + $headerBackgroundImagePosition: string, }>` overflow: visible; - ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} border-radius: 0; + ${props => getBackgroundStyle({ + background: props.$backgroundColor, + backgroundImage: props.$headerBackgroundImage, + backgroundImageSize: props.$headerBackgroundImageSize, + backgroundImageRepeat: props.$headerBackgroundImageRepeat, + backgroundImageOrigin: props.$headerBackgroundImageOrigin, + backgroundImagePosition: props.$headerBackgroundImagePosition, + })} `; const SiderInnerGrid = styled(InnerGrid)<{ @@ -70,25 +82,40 @@ const SiderInnerGrid = styled(InnerGrid)<{ $siderBackgroundImageOrigin: string; }>` overflow: auto; - ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} border-radius: 0; - ${(props) => props.$siderBackgroundImage && `background-image: url(${props.$siderBackgroundImage});`} - ${(props) => props.$siderBackgroundImageRepeat && `background-repeat: ${props.$siderBackgroundImageRepeat};`} - ${(props) => props.$siderBackgroundImageSize && `background-size: ${props.$siderBackgroundImageSize};`} - ${(props) => props.$siderBackgroundImagePosition && `background-position: ${props.$siderBackgroundImagePosition};`} - ${(props) => props.$siderBackgroundImageOrigin && `background-origin: ${props.$siderBackgroundImageOrigin};`} + ${props => getBackgroundStyle({ + background: props.$backgroundColor, + backgroundImage: props.$siderBackgroundImage, + backgroundImageSize: props.$siderBackgroundImageSize, + backgroundImageRepeat: props.$siderBackgroundImageRepeat, + backgroundImageOrigin: props.$siderBackgroundImageOrigin, + backgroundImagePosition: props.$siderBackgroundImagePosition, + })} `; const BodyInnerGrid = styled(InnerGrid)<{ $showBorder: boolean; - $backgroundColor: string; $borderColor: string; $borderWidth: string; + $backgroundColor: string; + $bodyBackgroundImage: string; + $bodyBackgroundImageRepeat: string; + $bodyBackgroundImageSize: string; + $bodyBackgroundImagePosition: string; + $bodyBackgroundImageOrigin: string; }>` border-top: ${(props) => `${props.$showBorder ? props.$borderWidth : 0} solid ${props.$borderColor}`}; flex: 1; - ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} border-radius: 0; + + ${props => getBackgroundStyle({ + background: props.$backgroundColor, + backgroundImage: props.$bodyBackgroundImage, + backgroundImageSize: props.$bodyBackgroundImageSize, + backgroundImageRepeat: props.$bodyBackgroundImageRepeat, + backgroundImageOrigin: props.$bodyBackgroundImageOrigin, + backgroundImagePosition: props.$bodyBackgroundImagePosition, + })} `; const FooterInnerGrid = styled(InnerGrid)<{ @@ -104,13 +131,15 @@ const FooterInnerGrid = styled(InnerGrid)<{ }>` border-top: ${(props) => `${props.$showBorder ? props.$borderWidth : 0} solid ${props.$borderColor}`}; overflow: visible; - ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} border-radius: 0; - ${(props) => props.$footerBackgroundImage && `background-image: url(${props.$footerBackgroundImage});`} - ${(props) => props.$footerBackgroundImageRepeat && `background-repeat: ${props.$footerBackgroundImageRepeat};`} - ${(props) => props.$footerBackgroundImageSize && `background-size: ${props.$footerBackgroundImageSize};`} - ${(props) => props.$footerBackgroundImagePosition && `background-position: ${props.$footerBackgroundImagePosition};`} - ${(props) => props.$footerBackgroundImageOrigin && `background-origin: ${props.$footerBackgroundImageOrigin};`} + ${props => getBackgroundStyle({ + background: props.$backgroundColor, + backgroundImage: props.$footerBackgroundImage, + backgroundImageSize: props.$footerBackgroundImageSize, + backgroundImageRepeat: props.$footerBackgroundImageRepeat, + backgroundImageOrigin: props.$footerBackgroundImageOrigin, + backgroundImagePosition: props.$footerBackgroundImagePosition, + })} `; export type LayoutProps = LayoutViewProps & { @@ -205,10 +234,15 @@ export function PageLayout(props: LayoutProps & { siderCollapsed: boolean; setSi horizontalGridCells={horizontalGridCells} autoHeight={true} emptyRows={5} - minHeight="46px" + minHeight="60px" containerPadding={[0, 0]} showName={{ bottom: showFooter ? 20 : 0 }} $backgroundColor={headerStyle?.headerBackground || 'transparent'} + $headerBackgroundImage={headerStyle?.headerBackgroundImage} + $headerBackgroundImageRepeat={headerStyle?.headerBackgroundImageRepeat} + $headerBackgroundImageSize={headerStyle?.headerBackgroundImageSize} + $headerBackgroundImagePosition={headerStyle?.headerBackgroundImagePosition} + $headerBackgroundImageOrigin={headerStyle?.headerBackgroundImageOrigin} style={{ padding: headerStyle.containerHeaderPadding }} /> @@ -271,6 +305,11 @@ export function PageLayout(props: LayoutProps & { siderCollapsed: boolean; setSi containerPadding={[0, 0]} hintPlaceholder={props.hintPlaceholder ?? HintPlaceHolder} $backgroundColor={bodyStyle?.background || 'transparent'} + $bodyBackgroundImage={bodyStyle?.backgroundImage} + $bodyBackgroundImageRepeat={bodyStyle?.backgroundImageRepeat} + $bodyBackgroundImageSize={bodyStyle?.backgroundImageSize} + $bodyBackgroundImagePosition={bodyStyle?.backgroundImagePosition} + $bodyBackgroundImageOrigin={bodyStyle?.backgroundImageOrigin} $borderColor={style?.border} $borderWidth={style?.borderWidth} style={{ padding: bodyStyle.containerBodyPadding }} /> @@ -335,6 +374,11 @@ export function PageLayout(props: LayoutProps & { siderCollapsed: boolean; setSi containerPadding={[0, 0]} hintPlaceholder={props.hintPlaceholder ?? HintPlaceHolder} $backgroundColor={bodyStyle?.background || 'transparent'} + $bodyBackgroundImage={bodyStyle?.backgroundImage} + $bodyBackgroundImageRepeat={bodyStyle?.backgroundImageRepeat} + $bodyBackgroundImageSize={bodyStyle?.backgroundImageSize} + $bodyBackgroundImagePosition={bodyStyle?.backgroundImagePosition} + $bodyBackgroundImageOrigin={bodyStyle?.backgroundImageOrigin} $borderColor={style?.border} $borderWidth={style?.borderWidth} style={{ padding: bodyStyle.containerBodyPadding }} /> diff --git a/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx b/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx index c51ffb073..c37af57cd 100644 --- a/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx @@ -43,6 +43,7 @@ import { EditorContext } from "comps/editorState"; import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils"; import { DisabledContext } from "comps/generators/uiCompBuilder"; import SliderControl from "@lowcoder-ee/comps/controls/sliderControl"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const RowWrapper = styled(Row)<{ $style: ResponsiveLayoutRowStyleType; @@ -56,13 +57,12 @@ const RowWrapper = styled(Row)<{ border-color: ${(props) => props.$style?.border}; border-style: ${(props) => props.$style?.borderStyle}; padding: ${(props) => props.$style.padding}; - background-color: ${(props) => props.$style.background}; rotate: ${props=> props.$style.rotation} overflow: ${(props) => (props.$showScrollbar ? 'auto' : 'hidden')}; ::-webkit-scrollbar { display: ${(props) => (props.$showScrollbar ? 'block' : 'none')}; - } - + } + ${props => getBackgroundStyle(props.$style)} `; const ColWrapper = styled(Col)<{ @@ -77,13 +77,13 @@ const ColWrapper = styled(Col)<{ > div { height: ${(props) => props.$matchColumnsHeight ? '100%' : 'auto'}; - background-color: ${(props) => props.$style?.background} !important; border-radius: ${(props) => props.$style?.radius}; border-width: ${(props) => props.$style?.borderWidth}px; border-color: ${(props) => props.$style?.border}; border-style: ${(props) => props.$style?.borderStyle}; margin: ${(props) => props.$style?.margin}; padding: ${(props) => props.$style?.padding}; + ${props => props.$style && getBackgroundStyle(props.$style)} } `; diff --git a/client/packages/lowcoder/src/comps/comps/richTextEditorComp.tsx b/client/packages/lowcoder/src/comps/comps/richTextEditorComp.tsx index c0f25aad2..52829fd49 100644 --- a/client/packages/lowcoder/src/comps/comps/richTextEditorComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/richTextEditorComp.tsx @@ -98,7 +98,7 @@ const commonStyle = (style: RichTextEditorStyleType, contentScrollBar: boolean) &.ql-container, &.ql-toolbar { border-color: ${style.border}; - background-color: ${style.background}; + background: ${style.background}; } } @@ -108,7 +108,7 @@ const commonStyle = (style: RichTextEditorStyleType, contentScrollBar: boolean) } & .ql-container { border-radius: 0 0 ${style.radius} ${style.radius}; - background-color: ${style.background}; + background: ${style.background}; border-width: ${style.borderWidth ? style.borderWidth : "1px"}; } `; diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx index a1839f9cb..78724cab5 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx @@ -5,7 +5,7 @@ import { ChangeEventHandlerControl } from "comps/controls/eventHandlerControl"; import { LabelControl } from "comps/controls/labelControl"; import { SelectOptionControl } from "comps/controls/optionsControl"; import { styleControl } from "comps/controls/styleControl"; -import { AnimationStyle, SegmentStyle, SegmentStyleType } from "comps/controls/styleControlConstants"; +import { AnimationStyle, LabelStyle, SegmentStyle, SegmentStyleType } from "comps/controls/styleControlConstants"; import styled, { css } from "styled-components"; import { UICompBuilder } from "../../generators"; import { CommonNameConfig, NameConfig, withExposingConfigs } from "../../generators/withExposing"; @@ -44,7 +44,7 @@ const getStyle = (style: SegmentStyleType) => { } .ant-segmented-item-selected, .ant-segmented-thumb { - background-color: ${style.indicatorBackground}; + background: ${style.indicatorBackground}; } } @@ -77,6 +77,7 @@ const SegmentChildrenMap = { onEvent: ChangeEventHandlerControl, options: SelectOptionControl, style: styleControl(SegmentStyle, 'style'), + labelStyle: styleControl(LabelStyle , 'labelStyle'), animationStyle: styleControl(AnimationStyle, 'animationStyle'), viewRef: RefControl, @@ -93,6 +94,7 @@ let SegmentedControlBasicComp = (function () { return props.label({ required: props.required, style: props.style, + labelStyle: props.labelStyle, animationStyle: props.animationStyle, children: ( {children.style.getPropertyView()}
+
+ {children.labelStyle.getPropertyView()} +
{children.animationStyle.getPropertyView()}
diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx index f20c66001..eef8cad60 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx @@ -55,6 +55,7 @@ let SelectBasicComp = (function () { dispatch={dispatch} /> ), + showValidationWhenEmpty: props.showValidationWhenEmpty, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx index 360a68460..6ad51d255 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx @@ -109,7 +109,7 @@ export const getStyle = ( } .ant-select-selector { - background-color: ${style.background}; + background: ${style.background}; border-color: ${style.border}; border-width:${(style as SelectStyleType).borderWidth}; box-shadow:${(style as SelectStyleType).boxShadow} ${(style as SelectStyleType).boxShadowColor}; @@ -124,7 +124,7 @@ export const getStyle = ( .ant-select-arrow, .ant-select-clear { - background-color: ${style.background}; + // background: ${style.background}; color: ${style.text === "#222222" ? "#8B8FA3" : isDarkColor(style.text) @@ -142,7 +142,7 @@ export const getStyle = ( &.ant-select-multiple .ant-select-selection-item { border: none; - background-color: ${(style as MultiSelectStyleType).tags}; + background: ${(style as MultiSelectStyleType).tags}; color: ${(style as MultiSelectStyleType).tagsText}; border-radius: ${style.radius}; @@ -191,7 +191,7 @@ const Select = styled(AntdSelect) <{ $style: SelectStyleType & MultiSelectStyleT `; const DropdownStyled = styled.div<{ $style: ChildrenMultiSelectStyleType }>` - background-color: ${props => props.$style?.background}; + background: ${props => props.$style?.background}; border: ${props => props.$style?.border}; border-style: ${props => props.$style?.borderStyle}; border-width: ${props => props.$style?.borderWidth}; diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectInputConstants.tsx index c69c35605..75f275c3c 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectInputConstants.tsx @@ -18,6 +18,7 @@ import { blurMethod, focusWithOptions } from "comps/utils/methodUtils"; export const SelectInputValidationChildren = { required: BoolControl, + showValidationWhenEmpty: BoolControl, customRule: CustomRuleControl, }; type ValidationComp = RecordConstructorToComp; @@ -122,6 +123,9 @@ export const SelectInputInvalidConfig = depsConfig< export const SelectInputValidationSection = (children: ValidationComp) => (
{requiredPropertyView(children)} + {children.showValidationWhenEmpty.propertyView({ + label: trans("prop.showEmptyValidation"), + })} {children.customRule.propertyView({})}
); diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx index 5db05650e..3dc80ed80 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/stepControl.tsx @@ -19,6 +19,7 @@ import { dropdownControl } from "comps/controls/dropdownControl"; import { useContext, useState, useEffect } from "react"; import { EditorContext } from "comps/editorState"; import { AutoHeightControl } from "@lowcoder-ee/index.sdk"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const sizeOptions = [ { @@ -120,14 +121,9 @@ let StepControlBasicComp = (function () { margin: ${props.style.margin}; rotate: ${props.style.rotation}; padding: ${props.style.padding}; - background-color: ${props.style.background}; border: ${props.style.borderWidth} solid ${props.style.border}; border-radius: ${props.style.radius}; - background-image: url(${props.style.backgroundImage}); - background-repeat: ${props.style.backgroundImageRepeat}; - background-size: ${props.style.backgroundImageSize}; - background-position: ${props.style.backgroundImagePosition}; - background-origin: ${props.style.backgroundImageOrigin}; + ${getBackgroundStyle(props.style)} .ant-steps-item { padding-top: 5px !important; } .ant-steps.ant-steps-label-vertical.ant-steps-small .ant-steps-item-icon { margin-top: 17px !important; } .ant-steps.ant-steps-label-vertical.ant-steps-default .ant-steps-item-icon { margin-top: 12px !important; } diff --git a/client/packages/lowcoder/src/comps/comps/shapeComp/shapeTriContainer.tsx b/client/packages/lowcoder/src/comps/comps/shapeComp/shapeTriContainer.tsx index 181b89887..6d083db1b 100644 --- a/client/packages/lowcoder/src/comps/comps/shapeComp/shapeTriContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/shapeComp/shapeTriContainer.tsx @@ -11,6 +11,7 @@ import { } from "../containerComp/containerView"; import { TriContainerViewProps } from "../triContainerComp/triContainerCompBuilder"; import { Coolshape } from "coolshapes-react"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const getStyle = (style: ContainerStyleType) => { return css` @@ -19,16 +20,8 @@ const getStyle = (style: ContainerStyleType) => { border-radius: ${style.radius}; overflow: hidden; padding: ${style.padding}; - ${style.background && `background-color: ${style.background};`} - ${style.backgroundImage && `background-image: ${style.backgroundImage};`} - ${style.backgroundImageRepeat && - `background-repeat: ${style.backgroundImageRepeat};`} - ${style.backgroundImageSize && - `background-size: ${style.backgroundImageSize};`} - ${style.backgroundImagePosition && - `background-position: ${style.backgroundImagePosition};`} - ${style.backgroundImageOrigin && - `background-origin: ${style.backgroundImageOrigin};`} + + ${getBackgroundStyle(style)} `; }; diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx index f68edddaf..a712fe7a8 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx @@ -29,7 +29,7 @@ import { BackgroundColorContext } from "comps/utils/backgroundColorContext"; import { PrimaryColor } from "constants/style"; import { trans } from "i18n"; import _ from "lodash"; -import { darkenColor, isDarkColor, ScrollBar } from "lowcoder-design"; +import { darkenColor, isDarkColor, isValidColor, ScrollBar } from "lowcoder-design"; import React, { Children, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { Resizable } from "react-resizable"; import styled, { css } from "styled-components"; @@ -52,7 +52,7 @@ import { ThemeContext } from "@lowcoder-ee/comps/utils/themeContext"; export const EMPTY_ROW_KEY = 'empty_row'; function genLinerGradient(color: string) { - return `linear-gradient(${color}, ${color})`; + return isValidColor(color) ? `linear-gradient(${color}, ${color})` : color; } const getStyle = ( @@ -72,62 +72,53 @@ const getStyle = ( } .ant-table-tbody { > tr:nth-of-type(2n + 1) { - &, - > td { - background: ${genLinerGradient(rowStyle.background)}; - // border-bottom:${rowStyle.borderWidth} ${rowStyle.borderStyle} ${rowStyle.border} !important; - // border-right:${rowStyle.borderWidth} ${rowStyle.borderStyle} ${rowStyle.border} !important; - } + background: ${genLinerGradient(rowStyle.background)}; } > tr:nth-of-type(2n) { - &, - > td { - background: ${alternateBackground}; - // border-bottom:${rowStyle.borderWidth} ${rowStyle.borderStyle} ${rowStyle.border} !important; - // border-right:${rowStyle.borderWidth} ${rowStyle.borderStyle} ${rowStyle.border} !important; - } + background: ${alternateBackground}; } // selected row > tr:nth-of-type(2n + 1).ant-table-row-selected { - > td { - background: ${selectedRowBackground}, ${rowStyle.background} !important; + background: ${selectedRowBackground}, ${rowStyle.background} !important; + > td.ant-table-cell { + background: transparent !important; } - > td.ant-table-cell-row-hover, - &:hover > td { + // > td.ant-table-cell-row-hover, + &:hover { background: ${hoverRowBackground}, ${selectedRowBackground}, ${rowStyle.background} !important; } } > tr:nth-of-type(2n).ant-table-row-selected { - > td { - background: ${selectedRowBackground}, ${alternateBackground} !important; + background: ${selectedRowBackground}, ${alternateBackground} !important; + > td.ant-table-cell { + background: transparent !important; } - > td.ant-table-cell-row-hover, - &:hover > td { + // > td.ant-table-cell-row-hover, + &:hover { background: ${hoverRowBackground}, ${selectedRowBackground}, ${alternateBackground} !important; } } // hover row - > tr:nth-of-type(2n + 1) > td.ant-table-cell-row-hover { - &, - > div:nth-of-type(2) { - background: ${hoverRowBackground}, ${rowStyle.background} !important; + > tr:nth-of-type(2n + 1):hover { + background: ${hoverRowBackground}, ${rowStyle.background} !important; + > td.ant-table-cell-row-hover { + background: transparent; } } - - > tr:nth-of-type(2n) > td.ant-table-cell-row-hover { - &, - > div:nth-of-type(2) { - background: ${hoverRowBackground}, ${alternateBackground} !important; + > tr:nth-of-type(2n):hover { + background: ${hoverRowBackground}, ${alternateBackground} !important; + > td.ant-table-cell-row-hover { + background: transparent; } } - > tr.ant-table-expanded-row > td { + > tr.ant-table-expanded-row { background: ${background}; } } @@ -255,9 +246,11 @@ const TableWrapper = styled.div<{ z-index: 99; ` } + > tr { + background: ${(props) => props.$headerStyle.headerBackground}; + } > tr > th { - background-color: ${(props) => props.$headerStyle.headerBackground}; - + background: transparent; border-color: ${(props) => props.$headerStyle.border}; border-width: ${(props) => props.$headerStyle.borderWidth}; color: ${(props) => props.$headerStyle.headerText}; @@ -600,12 +593,10 @@ function TableCellView(props: { rowHeight: rowHeight, } let { background } = style; - if (rowContext.selected) { - background = genLinerGradient(handleToSelectedRow(background)) + "," + background; - } if (rowContext.hover) { - background = genLinerGradient(handleToHoverRow(background)) + "," + background; + background = 'transparent'; } + tdView = ( ` + ${props => `background: ${props.$background}`}; + td:last-child { border-right: unset !important; } @@ -143,7 +146,7 @@ function TableSummaryCellView(props: { } = props; const style = { - background: cellColor || columnStyle.background || rowStyle.background, + background: cellColor || columnStyle.background, margin: columnStyle.margin || rowStyle.margin, text: columnStyle.text || rowStyle.text, border: columnStyle.border || rowStyle.border, @@ -174,6 +177,7 @@ function TableSummaryCellView(props: { export function TableSummary(props: { tableSize: string; expandableRows: boolean; + multiSelectEnabled: boolean; summaryRows: number; columns: ColumnComp[]; summaryRowStyle: TableSummaryRowStyleType; @@ -185,19 +189,27 @@ export function TableSummary(props: { summaryRowStyle, tableSize, expandableRows, + multiSelectEnabled, istoolbarPositionBelow, } = props; let visibleColumns = columns.filter(col => !col.getView().hide); if (expandableRows) { visibleColumns.unshift(new ColumnComp({})); } + if (multiSelectEnabled) { + visibleColumns.unshift(new ColumnComp({})); + } if (!visibleColumns.length) return <>; return ( {Array.from(Array(summaryRows)).map((_, rowIndex) => ( - + {visibleColumns.map((column, index) => { const summaryColumn = column.children.summaryColumns.getView()[rowIndex].getView(); return ( diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx index 67b96cfe9..85703fd11 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx @@ -54,7 +54,7 @@ const getStyle = ( fixedToolbar: boolean, ) => { return css` - background-color: ${style.background}; + background: ${style.background}; // Implement horizontal scrollbar and vertical page number selection is not blocked padding: 13px 12px; position: sticky; diff --git a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx index 0f3ebef67..a1fe29716 100644 --- a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx @@ -36,6 +36,7 @@ import { messageInstance } from "lowcoder-design/src/components/GlobalInstances" import { BoolControl } from "comps/controls/boolControl"; import { PositionControl } from "comps/controls/dropdownControl"; import SliderControl from "@lowcoder-ee/comps/controls/sliderControl"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const EVENT_OPTIONS = [ { @@ -82,25 +83,27 @@ const getStyle = ( border: ${style.borderWidth} ${style.borderStyle} ${style.border}; border-radius: ${style.radius}; padding: ${style.padding}; - background-color: ${style.background}; - background-image: url(${style.backgroundImage}); - background-repeat: ${style.backgroundImageRepeat}; - background-size: ${style.backgroundImageSize}; - background-position: ${style.backgroundImagePosition}; - background-origin: ${style.backgroundImageOrigin}; + ${getBackgroundStyle(style)} > .ant-tabs-content-holder > .ant-tabs-content > .ant-tabs-tabpane { height: 100%; .react-grid-layout { border-radius: 0; - background-color: ${bodyStyle.background || 'transparent'}; padding: ${bodyStyle.containerBodyPadding}; + ${getBackgroundStyle(bodyStyle)} } } > .ant-tabs-nav { - background-color: ${headerStyle.headerBackground || 'transparent'}; padding: ${headerStyle.containerHeaderPadding}; + ${getBackgroundStyle({ + background: headerStyle.headerBackground, + backgroundImage: headerStyle.headerBackgroundImage, + backgroundImageSize: headerStyle.headerBackgroundImageSize, + backgroundImageRepeat: headerStyle.headerBackgroundImageRepeat, + backgroundImageOrigin: headerStyle.headerBackgroundImageOrigin, + backgroundImagePosition: headerStyle.headerBackgroundImagePosition, + })} .ant-tabs-tab { div { @@ -159,7 +162,7 @@ const StyledTabs = styled(Tabs)<{ .ant-tabs-nav { display: ${(props) => (props.$showHeader ? "block" : "none")}; padding: 0 ${(props) => (props.$isMobile ? 16 : 24)}px; - background: white; + // background: white; margin: 0px; } diff --git a/client/packages/lowcoder/src/comps/comps/textComp.tsx b/client/packages/lowcoder/src/comps/comps/textComp.tsx index e8a695164..1dd20cae8 100644 --- a/client/packages/lowcoder/src/comps/comps/textComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textComp.tsx @@ -35,7 +35,7 @@ const getStyle = (style: TextStyleType) => { color: ${style.text}; text-transform:${style.textTransform} !important; text-decoration:${style.textDecoration} !important; - background-color: ${style.background}; + background: ${style.background}; .markdown-body a { color: ${style.links}; } diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx index cb241a087..6222fb6de 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx @@ -78,6 +78,7 @@ let InputBasicComp = new UICompBuilder(childrenMap, (props) => { labelStyle: props.labelStyle, inputFieldStyle:props.inputFieldStyle, animationStyle:props.animationStyle, + showValidationWhenEmpty: props.showValidationWhenEmpty, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx index 151f4fc7f..846a81a7d 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx @@ -82,6 +82,7 @@ let PasswordTmpComp = (function () { labelStyle: props.labelStyle, inputFieldStyle:props.inputFieldStyle, animationStyle:props.animationStyle, + showValidationWhenEmpty: props.showValidationWhenEmpty, ...validateState, }); }) @@ -106,6 +107,7 @@ let PasswordTmpComp = (function () { {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })}
{requiredPropertyView(children)} + {children.showValidationWhenEmpty.propertyView({label: trans("prop.showEmptyValidation")})} {regexPropertyView(children)} {minLengthPropertyView(children)} {maxLengthPropertyView(children)} diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx index 2a559dee3..a41fed3a2 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx @@ -99,6 +99,7 @@ let TextAreaTmpComp = (function () { style: props.style, labelStyle: props.labelStyle, animationStyle: props.animationStyle, + showValidationWhenEmpty: props.showValidationWhenEmpty, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 0cf64092a..9c3129746 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -144,6 +144,7 @@ export const textInputChildren = { // validation required: BoolControl, + showValidationWhenEmpty: BoolControl, minLength: NumberControl, maxLength: NumberControl, validationType: dropdownControl(TextInputValidationOptions, "Text"), @@ -226,6 +227,7 @@ export const TextInputInteractionSection = (children: TextInputComp) => ( export const TextInputValidationSection = (children: TextInputComp) => (
{requiredPropertyView(children)} + {children.showValidationWhenEmpty.propertyView({label: trans("prop.showEmptyValidation")})} {children.validationType.propertyView({ label: trans("prop.textType") })} {valueInfoMap[children.validationType.getView()]?.extra === undefined && regexPropertyView(children)} diff --git a/client/packages/lowcoder/src/comps/comps/timelineComp/timelineComp.tsx b/client/packages/lowcoder/src/comps/comps/timelineComp/timelineComp.tsx index 5854058af..762ba516a 100644 --- a/client/packages/lowcoder/src/comps/comps/timelineComp/timelineComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/timelineComp/timelineComp.tsx @@ -36,6 +36,7 @@ import { heightCalculator, widthCalculator, marginCalculator, + TimeLineStyleType, } from "comps/controls/styleControlConstants"; import { stateComp, valueComp } from "comps/generators/simpleGenerators"; import { @@ -47,6 +48,24 @@ import { timelineDate, timelineNode, TimelineDataTooltip } from "./timelineConst import { convertTimeLineData } from "./timelineUtils"; import { default as Timeline } from "antd/es/timeline"; import { EditorContext } from "comps/editorState"; +import { styled } from "styled-components"; + +const TimelineWrapper = styled.div<{ + $style: TimeLineStyleType +}>` + ${props => `margin: ${props.$style.margin ?? '3px'}` }; + ${props => `padding: ${props.$style.padding !== '3px' ? props.$style.padding : '20px 10px 0px 10px'}` }; + ${props => `width: ${widthCalculator(props.$style.margin ?? '3px')}` }; + ${props => `height: ${heightCalculator(props.$style.margin ?? '3px')}` }; + ${props => `background: ${props.$style.background}` }; + ${props => `border-radius: ${props.$style.radius}` }; + overflow: auto; + overflow-x: hidden; + + .ant-timeline .ant-timeline-item-head { + background-color: transparent; + } +`; const EventOptions = [ clickEvent, @@ -139,18 +158,7 @@ const TimelineComp = ( return ( -
+ -
+
); }; diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx index aa117ff5c..dda409de1 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx @@ -130,6 +130,7 @@ const TreeCompView = (props: RecordConstructorToView) => { ), + showValidationWhenEmpty: props.showValidationWhenEmpty, }); }; diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx index dbbf931c5..c1a3f8157 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx @@ -141,6 +141,7 @@ const TreeCompView = ( onBlur={() => props.onEvent("blur")} /> ), + showValidationWhenEmpty: props.showValidationWhenEmpty, }); }; diff --git a/client/packages/lowcoder/src/comps/comps/triContainerComp/triContainer.tsx b/client/packages/lowcoder/src/comps/comps/triContainerComp/triContainer.tsx index e9ba0b991..206864892 100644 --- a/client/packages/lowcoder/src/comps/comps/triContainerComp/triContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/triContainerComp/triContainer.tsx @@ -7,6 +7,7 @@ import styled, { css } from "styled-components"; import { checkIsMobile } from "util/commonUtils"; import { gridItemCompToGridItems, InnerGrid } from "../containerComp/containerView"; import { TriContainerViewProps } from "../triContainerComp/triContainerCompBuilder"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const getStyle = (style: ContainerStyleType) => { return css` @@ -16,12 +17,7 @@ const getStyle = (style: ContainerStyleType) => { border-style: ${style.borderStyle}; overflow: hidden; padding: ${style.padding}; - ${style.background && `background-color: ${style.background};`} - ${style.backgroundImage && `background-image: url(${style.backgroundImage});`} - ${style.backgroundImageRepeat && `background-repeat: ${style.backgroundImageRepeat};`} - ${style.backgroundImageSize && `background-size: ${style.backgroundImageSize};`} - ${style.backgroundImagePosition && `background-position: ${style.backgroundImagePosition};`} - ${style.backgroundImageOrigin && `background-origin: ${style.backgroundImageOrigin};`} + ${getBackgroundStyle(style)} `; }; @@ -44,13 +40,15 @@ const HeaderInnerGrid = styled(InnerGrid)<{ $headerBackgroundImageOrigin: string; }>` overflow: visible; - ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} border-radius: 0; - ${(props) => props.$headerBackgroundImage && `background-image: url(${props.$headerBackgroundImage});`} - ${(props) => props.$headerBackgroundImageRepeat && `background-repeat: ${props.$headerBackgroundImageRepeat};`} - ${(props) => props.$headerBackgroundImageSize && `background-size: ${props.$headerBackgroundImageSize};`} - ${(props) => props.$headerBackgroundImagePosition && `background-position: ${props.$headerBackgroundImagePosition};`} - ${(props) => props.$headerBackgroundImageOrigin && `background-origin: ${props.$headerBackgroundImageOrigin};`} + ${props => getBackgroundStyle({ + background: props.$backgroundColor, + backgroundImage: props.$headerBackgroundImage, + backgroundImageSize: props.$headerBackgroundImageSize, + backgroundImageRepeat: props.$headerBackgroundImageRepeat, + backgroundImageOrigin: props.$headerBackgroundImageOrigin, + backgroundImagePosition: props.$headerBackgroundImagePosition, + })} `; const BodyInnerGrid = styled(InnerGrid)<{ @@ -66,13 +64,15 @@ const BodyInnerGrid = styled(InnerGrid)<{ }>` border-top: ${(props) => `${props.$showBorder ? props.$borderWidth : 0} solid ${props.$borderColor}`}; flex: 1; - ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} border-radius: 0; - ${(props) => props.$backgroundImage && `background-image: url(${props.$backgroundImage});`} - ${(props) => props.$backgroundImageRepeat && `background-repeat: ${props.$backgroundImageRepeat};`} - ${(props) => props.$backgroundImageSize && `background-size: ${props.$backgroundImageSize};`} - ${(props) => props.$backgroundImagePosition && `background-position: ${props.$backgroundImagePosition};`} - ${(props) => props.$backgroundImageOrigin && `background-origin: ${props.$backgroundImageOrigin};`} + ${props => getBackgroundStyle({ + background: props.$backgroundColor, + backgroundImage: props.$backgroundImage, + backgroundImageSize: props.$backgroundImageSize, + backgroundImageRepeat: props.$backgroundImageRepeat, + backgroundImageOrigin: props.$backgroundImageOrigin, + backgroundImagePosition: props.$backgroundImagePosition, + })} `; const FooterInnerGrid = styled(InnerGrid)<{ @@ -88,13 +88,15 @@ const FooterInnerGrid = styled(InnerGrid)<{ }>` border-top: ${(props) => `${props.$showBorder ? props.$borderWidth : 0} solid ${props.$borderColor}`}; overflow: visible; - ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} border-radius: 0; - ${(props) => props.$footerBackgroundImage && `background-image: url(${props.$footerBackgroundImage});`} - ${(props) => props.$footerBackgroundImageRepeat && `background-repeat: ${props.$footerBackgroundImageRepeat};`} - ${(props) => props.$footerBackgroundImageSize && `background-size: ${props.$footerBackgroundImageSize};`} - ${(props) => props.$footerBackgroundImagePosition && `background-position: ${props.$footerBackgroundImagePosition};`} - ${(props) => props.$footerBackgroundImageOrigin && `background-origin: ${props.$footerBackgroundImageOrigin};`} + ${props => getBackgroundStyle({ + background: props.$backgroundColor, + backgroundImage: props.$footerBackgroundImage, + backgroundImageSize: props.$footerBackgroundImageSize, + backgroundImageRepeat: props.$footerBackgroundImageRepeat, + backgroundImageOrigin: props.$footerBackgroundImageOrigin, + backgroundImagePosition: props.$footerBackgroundImagePosition, + })} `; export type TriContainerProps = TriContainerViewProps & { diff --git a/client/packages/lowcoder/src/comps/comps/triContainerComp/triFloatTextContainer.tsx b/client/packages/lowcoder/src/comps/comps/triContainerComp/triFloatTextContainer.tsx index ee1fa3248..457a807ba 100644 --- a/client/packages/lowcoder/src/comps/comps/triContainerComp/triFloatTextContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/triContainerComp/triFloatTextContainer.tsx @@ -16,6 +16,7 @@ import { InnerGrid, } from "../containerComp/containerView"; import { TriContainerViewProps } from "../triContainerComp/triContainerCompBuilder"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const getStyle = (style: TextContainerStyleType) => { return css` @@ -28,7 +29,8 @@ const getStyle = (style: TextContainerStyleType) => { font-style:${style.fontStyle} !important; text-transform:${style.textTransform} !important; text-decoration:${style.textDecoration} !important; - background-color: ${style.background}; + ${getBackgroundStyle(style)} + .markdown-body a { color: ${style.links}; } @@ -64,6 +66,7 @@ const getStyle = (style: TextContainerStyleType) => { color: #000000; } } + background-color: transparent; } `; } @@ -79,14 +82,9 @@ ${props=>props.$animationStyle&&props.$animationStyle} overflow-y: scroll; border: ${(props) => props.$style.borderWidth} ${(props) => (props.$style.borderStyle ? props.$style.borderStyle : "solid")} ${(props) => props.$style.border}; border-radius: ${(props) => props.$style.radius}; - background-color: ${(props) => props.$style.background}; padding: ${(props) => props.$style.padding}; margin: ${(props) => props.$style.margin}; - ${(props) => props.$style.backgroundImage && `background-image: url(${props.$style.backgroundImage});`} - ${(props) => props.$style.backgroundImageRepeat && `background-repeat: ${props.$style.backgroundImageRepeat};`} - ${(props) => props.$style.backgroundImageSize && `background-size: ${props.$style.backgroundImageSize};`} - ${(props) => props.$style.backgroundImagePosition && `background-position: ${props.$style.backgroundImagePosition};`} - ${(props) => props.$style.backgroundImageOrigin && `background-origin: ${props.$style.backgroundImageOrigin};`} + ${props => getBackgroundStyle(props.$style)} `; const FloatTextWrapper = styled.div<{ $style: TextContainerStyleType, $horizontalAlignment : any }>` @@ -97,23 +95,47 @@ const FloatTextWrapper = styled.div<{ $style: TextContainerStyleType, $horizonta `; const HeaderInnerGrid = styled(InnerGrid)<{ - $backgroundColor: string + $backgroundColor: string, + $headerBackgroundImage: string, + $headerBackgroundImageSize: string, + $headerBackgroundImageRepeat: string, + $headerBackgroundImageOrigin: string, + $headerBackgroundImagePosition: string, }>` overflow: visible; - ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} border-radius: 0; + ${props => getBackgroundStyle({ + background: props.$backgroundColor, + backgroundImage: props.$headerBackgroundImage, + backgroundImageSize: props.$headerBackgroundImageSize, + backgroundImageRepeat: props.$headerBackgroundImageRepeat, + backgroundImageOrigin: props.$headerBackgroundImageOrigin, + backgroundImagePosition: props.$headerBackgroundImagePosition, + })} `; const BodyInnerGrid = styled(InnerGrid)<{ $showBorder: boolean; - $backgroundColor: string; $borderColor: string; $borderWidth: string; + $backgroundColor: string; + $bodyBackgroundImage: string; + $bodyBackgroundImageRepeat: string; + $bodyBackgroundImageSize: string; + $bodyBackgroundImagePosition: string; + $bodyBackgroundImageOrigin: string; }>` border-top: ${(props) => `${props.$showBorder ? props.$borderWidth : 0} solid ${props.$borderColor}`}; flex: 1; - ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} border-radius: 0; + ${props => getBackgroundStyle({ + background: props.$backgroundColor, + backgroundImage: props.$bodyBackgroundImage, + backgroundImageSize: props.$bodyBackgroundImageSize, + backgroundImageRepeat: props.$bodyBackgroundImageRepeat, + backgroundImageOrigin: props.$bodyBackgroundImageOrigin, + backgroundImagePosition: props.$bodyBackgroundImagePosition, + })} `; const FooterInnerGrid = styled(InnerGrid)<{ @@ -131,11 +153,14 @@ const FooterInnerGrid = styled(InnerGrid)<{ overflow: visible; ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} border-radius: 0; - ${(props) => props.$footerBackgroundImage && `background-image: url(${props.$footerBackgroundImage});`} - ${(props) => props.$footerBackgroundImageRepeat && `background-repeat: ${props.$footerBackgroundImageRepeat};`} - ${(props) => props.$footerBackgroundImageSize && `background-size: ${props.$footerBackgroundImageSize};`} - ${(props) => props.$footerBackgroundImagePosition && `background-position: ${props.$footerBackgroundImagePosition};`} - ${(props) => props.$footerBackgroundImageOrigin && `background-origin: ${props.$footerBackgroundImageOrigin};`} + ${props => getBackgroundStyle({ + background: props.$backgroundColor, + backgroundImage: props.$footerBackgroundImage, + backgroundImageSize: props.$footerBackgroundImageSize, + backgroundImageRepeat: props.$footerBackgroundImageRepeat, + backgroundImageOrigin: props.$footerBackgroundImageOrigin, + backgroundImagePosition: props.$footerBackgroundImagePosition, + })} `; export type TriContainerProps = TriContainerViewProps & { @@ -190,6 +215,11 @@ export function TriContainer(props: TriContainerProps) { containerPadding={[0, 0]} showName={{ bottom: showFooter ? 20 : 0 }} $backgroundColor={headerStyle?.headerBackground || 'transparent'} + $headerBackgroundImage={headerStyle?.headerBackgroundImage} + $headerBackgroundImageRepeat={headerStyle?.headerBackgroundImageRepeat} + $headerBackgroundImageSize={headerStyle?.headerBackgroundImageSize} + $headerBackgroundImagePosition={headerStyle?.headerBackgroundImagePosition} + $headerBackgroundImageOrigin={headerStyle?.headerBackgroundImageOrigin} style={{ padding: headerStyle.containerHeaderPadding}} /> )} @@ -208,6 +238,11 @@ export function TriContainer(props: TriContainerProps) { containerPadding={[0, 0]} hintPlaceholder={props.hintPlaceholder ?? HintPlaceHolder} $backgroundColor={bodyStyle?.background || 'transparent'} + $bodyBackgroundImage={bodyStyle?.backgroundImage} + $bodyBackgroundImageRepeat={bodyStyle?.backgroundImageRepeat} + $bodyBackgroundImageSize={bodyStyle?.backgroundImageSize} + $bodyBackgroundImagePosition={bodyStyle?.backgroundImagePosition} + $bodyBackgroundImageOrigin={bodyStyle?.backgroundImageOrigin} $borderColor={style?.border} $borderWidth={style?.borderWidth} style={{ diff --git a/client/packages/lowcoder/src/comps/controls/codeControl.tsx b/client/packages/lowcoder/src/comps/controls/codeControl.tsx index 33103f911..2ae75d7a0 100644 --- a/client/packages/lowcoder/src/comps/controls/codeControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/codeControl.tsx @@ -28,6 +28,7 @@ import { import { ControlPropertyViewWrapper, isValidColor, + isValidGradient, toHex, wrapperToControlItem, } from "lowcoder-design"; @@ -432,7 +433,7 @@ export function stringUnionControl( export const ColorCodeControl = codeControl( (value: unknown) => { const valueString = toString(value); - + if (valueString === "") { return valueString; } @@ -442,6 +443,9 @@ export const ColorCodeControl = codeControl( if (isThemeColorKey(valueString)) { return valueString; } + if (isValidGradient(valueString)) { + return valueString; + } throw new Error(`the argument must be type CSS color`); }, { @@ -465,6 +469,9 @@ export const ColorOrBoolCodeControl = codeControl( if (isThemeColorKey(valueString)) { return valueString; } + if (isValidGradient(valueString)) { + return valueString; + } throw new Error(`the argument must be type CSS color or Boolean`); }, { diff --git a/client/packages/lowcoder/src/comps/controls/colorControl.tsx b/client/packages/lowcoder/src/comps/controls/colorControl.tsx index 7eb7a24d9..6b45a982d 100644 --- a/client/packages/lowcoder/src/comps/controls/colorControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/colorControl.tsx @@ -1,9 +1,10 @@ import { ColorCodeControl } from "./codeControl"; import { ColorSelect, controlItem, ControlPropertyViewWrapper, IconDep } from "lowcoder-design"; import styled from "styled-components"; -import React, { useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { ControlParams } from "./controlParams"; import { trans } from "i18n"; +import { useThemeColors } from "@lowcoder-ee/util/hooks"; const ColorContainer = styled.div` display: inline-flex; @@ -72,6 +73,7 @@ type PropertyViewParam = { isDep?: boolean; // auto-generated message? depMsg?: string; + allowGradient?: boolean; }; export class ColorControl extends ColorCodeControl { @@ -93,6 +95,8 @@ function ColorItem(props: { const [focus, setFocus] = useState(false); const inputRef = React.createRef(); const containerRef = React.createRef(); + + const currentThemeColors = useThemeColors(param.allowGradient); const input = propertyView.call(controlThis, { placeholder: param.panelDefaultColor, @@ -112,6 +116,7 @@ function ColorItem(props: { }, [focus]); const color = controlThis.getView(); + useEffect(() => { setShowDep(param.isDep && !focus && !color); }, [color, focus, param.isDep]); @@ -134,6 +139,8 @@ function ColorItem(props: {
; animationStyle?: Record; onMouseDown?: React.MouseEventHandler; + showValidationWhenEmpty?: boolean; }; const StyledStarIcon = styled(StarIcon)` @@ -77,7 +78,7 @@ const MainWrapper = styled.div<{ props.$position === "column" && props.$hasLabel ? "calc(100% - 4px)" : "100%"}; display: flex; align-items: ${(props) => (props.$position === "row" ? "center" : "start")}; - flex-shrink: 0; + flex-shrink: 1; `; const LabelWrapper = styled.div<{ @@ -178,6 +179,12 @@ export const LabelControl = (function () { return new MultiCompBuilder(childrenMap, (props) => (args: LabelViewProps) => { + const inputValue = ( + ((args.children as ReactElement)?.props?.children as ReactElement)?.props?.value // text area comp + ?? (args.children as ReactElement)?.props.value?.value // number input comp + ?? (args.children as ReactElement)?.props.value + ); + return ( - {args.help && Boolean((args.children as ReactElement)?.props.value) && ( + {args.help && ( + args.showValidationWhenEmpty + || (!args.showValidationWhenEmpty && Boolean(inputValue)) + ) && ( , +) => { + const editorState = useContext(EditorContext); + const {comp, compType} = useContext(CompContext); + const theme = useContext(ThemeContext); + const bgColor = useContext(BackgroundColorContext); + const { themeId } = theme || {}; + const isPreviewTheme = themeId === 'preview-theme'; + const isDefaultTheme = themeId === 'default-theme-id'; + + const appSettingsComp = editorState?.getAppSettingsComp(); + const preventAppStylesOverwriting = appSettingsComp?.getView()?.preventAppStylesOverwriting; + const { appliedThemeId, preventStyleOverwriting } = (comp?.comp || {}); + const appTheme = isPreviewTheme || isDefaultTheme || (!preventStyleOverwriting && !preventAppStylesOverwriting) + ? theme?.theme + : defaultTheme; + let compTheme: JSONValue|undefined = {}; + if (appliedThemeId !== themeId) { + compTheme = isPreviewTheme || isDefaultTheme || (compType && !preventStyleOverwriting && !preventAppStylesOverwriting) + ? { + ...(omit(defaultTheme, 'components', 'chart')), + ...defaultTheme.components?.[compType]?.[styleKey] as unknown as Record, + ...(omit(theme?.theme, 'components', 'chart')), + ...theme?.theme?.components?.[compType]?.[styleKey] as unknown as Record, + } + : defaultTheme.components?.[compType]?.[styleKey]; + } + const styleProps = (!comp && !compType) || preventStyleOverwriting || preventAppStylesOverwriting || appliedThemeId === themeId + ? props + : {}; + + return { + appTheme, + styleProps, + bgColor, + compTheme, + compType, + } +}; + export function styleControl( colorConfigs: T, styleKey: string = '', @@ -888,39 +930,16 @@ export function styleControl( return new ControlItemCompBuilder( childrenMap as ToConstructor<{ [K in Names]: ColorControl }>, (props) => { - // const compType = useContext(CompTypeContext); - const editorState = useContext(EditorContext); - const {comp, compType} = useContext(CompContext); - const theme = useContext(ThemeContext); - const bgColor = useContext(BackgroundColorContext); - const { themeId } = theme || {}; - const isPreviewTheme = themeId === 'preview-theme'; - const isDefaultTheme = themeId === 'default-theme-id'; - - - const appSettingsComp = editorState?.getAppSettingsComp(); - const preventAppStylesOverwriting = appSettingsComp?.getView()?.preventAppStylesOverwriting; - const { appliedThemeId, preventStyleOverwriting } = (comp?.comp || {}); - const appTheme = isPreviewTheme || isDefaultTheme || (!preventStyleOverwriting && !preventAppStylesOverwriting) - ? theme?.theme - : defaultTheme; - let compTheme: JSONValue|undefined = {}; - if (appliedThemeId !== themeId) { - compTheme = isPreviewTheme || isDefaultTheme || (compType && !preventStyleOverwriting && !preventAppStylesOverwriting) - ? { - ...(omit(defaultTheme, 'components', 'chart')), - ...defaultTheme.components?.[compType]?.[styleKey] as unknown as Record, - ...(omit(theme?.theme, 'components', 'chart')), - ...theme?.theme?.components?.[compType]?.[styleKey] as unknown as Record, - } - : defaultTheme.components?.[compType]?.[styleKey]; - } - const styleProps = (!comp && !compType) || preventStyleOverwriting || preventAppStylesOverwriting || appliedThemeId === themeId - ? props as ColorMap - : {} as ColorMap; + const { + styleProps, + appTheme, + bgColor, + compTheme, + compType, + } = useThemeStyles(styleKey, props as Record); return calcColors( - styleProps, + styleProps as ColorMap, colorConfigs, appTheme, bgColor, @@ -932,21 +951,26 @@ export function styleControl( ) .setControlItemData({ filterText: label, searchChild: true }) .setPropertyViewFn((children) => { - const theme = useContext(ThemeContext); - const compType = useContext(CompTypeContext); - const bgColor = useContext(BackgroundColorContext); const isMobile = useIsMobile(); - const compTheme = compType - ? theme?.theme?.components?.[compType]?.[styleKey] - : undefined; + const childrenProps = childrenToProps(children) as Record; + const { + styleProps, + appTheme, + bgColor, + compTheme, + compType, + } = useThemeStyles(styleKey, childrenProps); const props = calcColors( - childrenToProps(children) as ColorMap, + styleProps as ColorMap, colorConfigs, - theme?.theme, + appTheme, bgColor, compTheme as Record | undefined, + compType, + styleKey, ); + const showReset = Object.values(childrenToProps(children)).findIndex((item) => item) > -1; return ( <> @@ -1341,6 +1365,7 @@ export function styleControl( isDep: true, depMsg: depMsg, + allowGradient: config.name.includes('background'), })}
); diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index d8d6d2422..fca765ca3 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -1353,7 +1353,6 @@ export const RadioStyle = [ ] as const; export const SegmentStyle = [ - LABEL, ...STYLING_FIELDS_SEQUENCE.filter( (style) => ["border", "borderWidth"].includes(style.name) === false ), @@ -1986,6 +1985,7 @@ export const RichTextEditorStyle = [ ] as const; export type QRCodeStyleType = StyleConfigType; +export type TimeLineStyleType = StyleConfigType; export type AvatarStyleType = StyleConfigType; export type AvatarLabelStyleType = StyleConfigType; export type AvatarContainerStyleType = StyleConfigType< diff --git a/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx b/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx index 43fa3e5b7..6c04fa8aa 100644 --- a/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/drawerComp.tsx @@ -136,7 +136,7 @@ let TmpDrawerComp = (function () { }, body: { padding: 0, - backgroundColor: props.style.background + background: props.style.background } }} title={props.title} diff --git a/client/packages/lowcoder/src/comps/hooks/modalComp.tsx b/client/packages/lowcoder/src/comps/hooks/modalComp.tsx index ed2710ed5..2a3235f9d 100644 --- a/client/packages/lowcoder/src/comps/hooks/modalComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/modalComp.tsx @@ -23,6 +23,7 @@ import { NameConfig, withExposingConfigs } from "../generators/withExposing"; import { BoolControl } from "comps/controls/boolControl"; import { withDefault } from "comps/generators"; import SliderControl from "../controls/sliderControl"; +import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; const EventOptions = [ { label: trans("modalComp.open"), value: "open", description: trans("modalComp.openDesc") }, @@ -35,13 +36,9 @@ const getStyle = (style: ModalStyleType, modalScrollbar: boolean) => { border-radius: ${style.radius}; border: ${style.borderWidth} solid ${style.border}; overflow: hidden; - background-color: ${style.background}; - ${style.backgroundImage ? `background-image: url(${style.backgroundImage}) !important; ` : ';'} - ${style.backgroundImageRepeat ? `background-repeat: ${style.backgroundImageRepeat};` : 'no-repeat;'} - ${style.backgroundImageSize ? `background-size: ${style.backgroundImageSize};` : 'cover'} - ${style.backgroundImagePosition ? `background-position: ${style.backgroundImagePosition};` : 'center;'} - ${style.backgroundImageOrigin ? `background-origin: ${style.backgroundImageOrigin};` : 'padding-box;'} margin: ${style.margin}; + ${getBackgroundStyle(style)} + .ant-modal-body > .react-resizable > .react-grid-layout { background-color: ${style.background}; } diff --git a/client/packages/lowcoder/src/comps/utils/appSettingContext.tsx b/client/packages/lowcoder/src/comps/utils/appSettingContext.tsx new file mode 100644 index 000000000..8b442b3d7 --- /dev/null +++ b/client/packages/lowcoder/src/comps/utils/appSettingContext.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +export type AppSettingType = 'setting' | 'canvas'; + +export const AppSettingContext = React.createContext<{ + settingType: AppSettingType; +}>({ settingType: 'setting' }); diff --git a/client/packages/lowcoder/src/constants/style.ts b/client/packages/lowcoder/src/constants/style.ts index c7787ea77..e21136c11 100644 --- a/client/packages/lowcoder/src/constants/style.ts +++ b/client/packages/lowcoder/src/constants/style.ts @@ -1,5 +1,5 @@ // brand color -export const PrimaryColor = "#3377ff"; +export const PrimaryColor = "#b85fff"; // module export const ModulePrimaryColor = "#F27A24"; @@ -21,10 +21,10 @@ export const DarkActiveTextColor = "#222222"; export const ActiveTextColor = "#315efb"; // icon -export const NormalMenuIconColor = "#8B8FA3"; +export const NormalMenuIconColor = "#b480de"; // Ui Comp -export const UiCompBorderRadius = "2px"; +export const UiCompBorderRadius = "4px"; // Tab export const TabActiveColor = "#222222"; diff --git a/client/packages/lowcoder/src/constants/themeConstants.ts b/client/packages/lowcoder/src/constants/themeConstants.ts index 827602d7d..144486380 100644 --- a/client/packages/lowcoder/src/constants/themeConstants.ts +++ b/client/packages/lowcoder/src/constants/themeConstants.ts @@ -1,4 +1,5 @@ import { ThemeDetail } from "@lowcoder-ee/api/commonSettingApi"; +import { DEFAULT_GRID_COLUMNS, DEFAULT_ROW_COUNT, DEFAULT_ROW_HEIGHT } from "@lowcoder-ee/layout/calculateUtils"; const theme = { primary: "#3377FF", @@ -13,7 +14,9 @@ const theme = { margin: "3px", padding: "3px", lineHeight: "18px", - gridColumns: "24", + gridColumns: String(DEFAULT_GRID_COLUMNS), + gridRowHeight: String(DEFAULT_ROW_HEIGHT), + gridRowCount: DEFAULT_ROW_COUNT, textSize: "14px", // text: "#222222", animation: "", @@ -25,6 +28,10 @@ const theme = { animationIterationCount: "", showComponentLoadingIndicators: true, showDataLoadingIndicators: true, + gridBgImageSize: "cover", + gridBgImagePosition: "center", + gridBgImageRepeat: "no-repeat", + gridBgImageOrigin: "padding-box", }; const text = { diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 108d3f8d4..61450a61f 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -163,6 +163,7 @@ export const en = { "showSearch": "Searchable", "defaultValue": "Default Value", "required": "Required Field", + "showEmptyValidation": "Show Validation On Empty/Reset", "readOnly": "Read Only", "readOnlyTooltip": "Read-only components appear normal but cannot be modified.", "minimum": "Minimum", @@ -2580,7 +2581,8 @@ export const en = { "chartClick": "Click", "chartVisit": "Visit", "chartQuery": "Query", - "chartBuy": "Buy" + "chartBuy": "Buy", + "canvas": "Canvas Settings", }, "themeDetail": { "primary": "Brand Color", @@ -2616,11 +2618,31 @@ export const en = { "paddingDesc": "Default padding typically used for most components", "containerHeaderPadding": "Header Padding", "containerheaderpaddingDesc": "Default header padding typically used for most components", - "gridColumns": "Canvas Grid Columns", + "gridColumns": "Grid Columns", "gridColumnsDesc": "Default number of columns typically used for most containers", "loadingIndicators": "Loading Indicators", "showComponentLoadingIndicators": "Show loading indicators when component load", - "showDataLoadingIndicators": "Show loading indicators when data load" + "showDataLoadingIndicators": "Show loading indicators when data load", + "background": "Background Styles", + "gridSettings": "Grid Settings", + "gridRowHeight": "Grid Row Height", + "gridRowHeightDesc": "Height of each row in grid", + "gridRowCount": "Grid Row Count", + "gridRowCountDesc": "Max. number of rows in grid", + "gridPaddingX": "Horizontal Padding", + "gridPaddingXDesc": "Canvas horizontal padding", + "gridPaddingY": "Vertical Padding", + "gridPaddingYDesc": "Canvas vertical padding", + "gridBgImage": "Background Image", + "gridBgImageDesc": "Canvas background image", + "gridBgImageRepeat": "Background Image Repeat", + "gridBgImageRepeatDesc": "Canvas background image repeat", + "gridBgImageSize": "Background Image Size", + "gridBgImageSizeDesc": "Canvas background image size", + "gridBgImagePosition": "Background Image Position", + "gridBgImagePositionDesc": "Canvas background image position", + "gridBgImageOrigin": "Background Image Origin", + "gridBgImageOriginDesc": "Canvas background image origin", }, "pluginSetting": { "title": "Plugins", @@ -2784,7 +2806,18 @@ export const en = { "appTitle": "Title", "appDescription": "Description", "appCategory": "Category", - "showPublicHeader": "Show header in public view" + "showPublicHeader": "Show header in public view", + "canvas": "Canvas Settings", + "gridColumns": "Grid Columns", + "gridRowHeight": "Grid Row Height", + "gridRowCount": "Grid Row Count", + "gridPaddingX": "Canvas Horizontal Padding", + "gridPaddingY": "Canvas Vertical Padding", + "gridBgImage": "Background Image", + "gridBgImageRepeat": "Background Image Repeat", + "gridBgImageSize": "Background Image Size", + "gridBgImagePosition": "Background Image Position", + "gridBgImageOrigin": "Background Image Origin" }, "customShortcut": { "title": "Custom Shortcuts", diff --git a/client/packages/lowcoder/src/layout/calculateUtils.tsx b/client/packages/lowcoder/src/layout/calculateUtils.tsx index 587130f2d..1d919bd1b 100644 --- a/client/packages/lowcoder/src/layout/calculateUtils.tsx +++ b/client/packages/lowcoder/src/layout/calculateUtils.tsx @@ -7,18 +7,9 @@ export type PositionParams = Pick< "margin" | "containerPadding" | "containerWidth" | "cols" | "rowHeight" | "maxRows" >; -// Added By Aqib Mirza -let gridColumns: number; - -const getDefaultGridColumns = () => { - return gridColumns; -}; - -export { getDefaultGridColumns }; -export const DEFAULT_GRID_COLUMNS = getDefaultGridColumns() || 24; -////////////////////// - +export const DEFAULT_GRID_COLUMNS = 24; export const DEFAULT_ROW_HEIGHT = 8; +export const DEFAULT_ROW_COUNT = Infinity; export const DEFAULT_POSITION_PARAMS: PositionParams = { margin: [0, 0], @@ -26,7 +17,7 @@ export const DEFAULT_POSITION_PARAMS: PositionParams = { containerWidth: 0, cols: DEFAULT_GRID_COLUMNS, rowHeight: DEFAULT_ROW_HEIGHT, - maxRows: Infinity, + maxRows: DEFAULT_ROW_COUNT, }; // Helper for generating column width @@ -102,6 +93,7 @@ export function calcGridItemSizePx( const width = calcGridItemWHPx(w, colWidth, margin[0], false); const isTouchSBound = top ? isTouchBound(maxRows, rowHeight, h, top) : false; + // console.log('positionParams',positionParams); const height = calcGridItemWHPx(h, rowHeight, margin[1], isTouchSBound); return { width, height }; } diff --git a/client/packages/lowcoder/src/layout/compSelectionWrapper.tsx b/client/packages/lowcoder/src/layout/compSelectionWrapper.tsx index fd812a8b2..a0923d486 100644 --- a/client/packages/lowcoder/src/layout/compSelectionWrapper.tsx +++ b/client/packages/lowcoder/src/layout/compSelectionWrapper.tsx @@ -35,7 +35,7 @@ const NameDiv = styled.div<{ } return "#B8B9BF"; }}; - border-radius: ${(props) => (props.$position === "top" ? "2px 2px 0 0" : "0 0 2px 2px")}; + border-radius: ${(props) => (props.$position === "top" ? "4px 4px 0 0" : "0 0 4px 4px")}; font-weight: 500; color: #ffffff; position: absolute; @@ -89,13 +89,15 @@ function getLineStyle( return ` border: ${GRID_ITEM_BORDER_WIDTH}px ${borderStyle} ${borderColor}; - padding: ${isHidden || !isSelected ? 0 : padding[1] - GRID_ITEM_BORDER_WIDTH}px; - padding-left: ${padding[0] - GRID_ITEM_BORDER_WIDTH}px; - padding-right: ${padding[0] - GRID_ITEM_BORDER_WIDTH}px; + padding: 0px; + // padding: ${isHidden || !isSelected ? 0 : padding[1] - GRID_ITEM_BORDER_WIDTH}px; + // padding-left: ${padding[0] - GRID_ITEM_BORDER_WIDTH}px; + // padding-right: ${padding[0] - GRID_ITEM_BORDER_WIDTH}px; `; } // padding: ${props => props.hover || props.showDashline ? 3 : 4}px; + const SelectableDiv = styled.div<{ $hover?: boolean; $showDashLine: boolean; @@ -173,7 +175,7 @@ const dragCss = (props: DragHandleProps, handle: ResizeHandleAxis) => css` height: 8px; width: 8px; border: 1px solid ${props.$compType === "module" ? ModulePrimaryColor : PrimaryColor}; - border-radius: 2px; + border-radius: 4px; background-color: #f5f5f6; z-index: 11; pointer-events: none; diff --git a/client/packages/lowcoder/src/layout/gridItem.tsx b/client/packages/lowcoder/src/layout/gridItem.tsx index 432c9b453..963c1c056 100644 --- a/client/packages/lowcoder/src/layout/gridItem.tsx +++ b/client/packages/lowcoder/src/layout/gridItem.tsx @@ -103,15 +103,15 @@ const ResizableStyled = styled(Resizable)<{ $zIndex: number, isDroppable : boole * An individual item within a ReactGridLayout. */ export const GridItem = React.memo((props: GridItemProps) => { - const position = useMemo(() => - calcGridItemPosition({ + const position = useMemo(() =>{ + return calcGridItemPosition({ margin: props.margin, containerPadding: props.containerPadding, containerWidth: props.containerWidth, cols: props.cols, rowHeight: props.rowHeight, maxRows: props.maxRows, - }, props.x, props.y, props.w, props.h), + }, props.x, props.y, props.w, props.h)}, [ props.margin, props.containerPadding, @@ -126,6 +126,7 @@ export const GridItem = React.memo((props: GridItemProps) => { calcGridItemPosition, ] ); + const [resizing, setResizing] = useState<{ width: number; height: number } | undefined>(); const [dragging, setDragging] = useState<{ top: number; left: number } | undefined>(); const elementRef = useRef(null); @@ -345,6 +346,7 @@ export const GridItem = React.memo((props: GridItemProps) => { Math.min(maxes.width, Infinity), Math.min(maxes.height, Infinity), ]; + return ( { * @return {String} Container height in pixels. */ containerHeight(): string { - const { margin, rowHeight } = this.props as Required; + const { margin, rowHeight, fixedRowCount } = this.props as Required; const { extraHeight, emptyRows } = this.props; const positionParams = genPositionParams(this.props); + const { containerPadding } = positionParams; const layout = this.getUILayout(undefined, true); let nbRow = bottom(layout); - if (!_.isNil(emptyRows) && _.size(layout) === 0) { - nbRow = emptyRows; + if (!_.isNil(emptyRows) && (_.size(layout) === 0 || fixedRowCount)) { + nbRow = emptyRows;// === Infinity ? 0 : emptyRows; } const containerHeight = Math.max( nbRow * rowHeight + (nbRow - 1) * margin[1] + containerPadding[1] * 2 @@ -341,7 +343,7 @@ class GridLayout extends React.Component { onLayoutMaybeChanged(newLayout: Layout, oldLayout?: Layout) { // log.debug("layout: layoutMayBeChanged. oldLayout: ", oldLayout, " newLayout: ", newLayout); if (!oldLayout) oldLayout = this.state.layout; - + if (!_.isEqual(oldLayout, newLayout)) { this.props.onLayoutChange?.(newLayout); } @@ -1046,6 +1048,7 @@ class GridLayout extends React.Component { $radius={this.props.radius} $autoHeight={this.props.autoHeight} $overflow={this.props.overflow} + $maxRows={this.props.emptyRows} tabIndex={-1} onDrop={isDroppable ? this.onDrop : _.noop} onDragLeave={isDroppable ? this.onDragLeave : _.noop} @@ -1081,15 +1084,19 @@ const LayoutContainer = styled.div<{ $bgColor?: string; $autoHeight?: boolean; $overflow?: string; + $maxRows?: number; $radius?: string; }>` border-radius: ${(props) => props.$radius ?? "4px"}; - background-color: ${(props) => props.$bgColor ?? "#f5f5f6"}; + // background-color: ${(props) => props.$bgColor ?? "#f5f5f6"}; /* height: 100%; */ height: ${(props) => (props.$autoHeight ? "auto" : "100%")}; - overflow: auto; - overflow: ${(props) => props.$overflow ?? "overlay"}; + overflow: ${(props) => + props.$maxRows !== DEFAULT_ROW_COUNT + ? 'hidden' + : props.$overflow ?? "overlay" + }; ${(props) => props.$autoHeight && `::-webkit-scrollbar { diff --git a/client/packages/lowcoder/src/layout/gridLayoutPropTypes.tsx b/client/packages/lowcoder/src/layout/gridLayoutPropTypes.tsx index e7e2708d5..735b2ea94 100644 --- a/client/packages/lowcoder/src/layout/gridLayoutPropTypes.tsx +++ b/client/packages/lowcoder/src/layout/gridLayoutPropTypes.tsx @@ -34,6 +34,7 @@ export type GridLayoutProps = { containerPadding?: [number, number]; rowHeight?: number; isRowCountLocked?: boolean; + fixedRowCount?: boolean; emptyRows?: number; maxRows?: number; isDraggable?: boolean; diff --git a/client/packages/lowcoder/src/pages/editor/editorView.tsx b/client/packages/lowcoder/src/pages/editor/editorView.tsx index 80a621514..d807771ef 100644 --- a/client/packages/lowcoder/src/pages/editor/editorView.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorView.tsx @@ -14,6 +14,8 @@ import { LeftSettingIcon, LeftStateIcon, LeftLayersIcon, + LeftColorPaletteIcon, + LeftJSSettingIcon, ScrollBar, } from "lowcoder-design"; import { useTemplateViewMode } from "util/hooks"; @@ -55,6 +57,7 @@ import { isAggregationApp } from "util/appUtils"; import EditorSkeletonView from "./editorSkeletonView"; import { getCommonSettings } from "@lowcoder-ee/redux/selectors/commonSettingSelectors"; import { isEqual, noop } from "lodash"; +import { AppSettingContext, AppSettingType } from "@lowcoder-ee/comps/utils/appSettingContext"; // import { BottomSkeleton } from "./bottom/BottomContent"; const Header = lazy( @@ -257,6 +260,8 @@ enum SiderKey { State = "state", Setting = "setting", Layout = "layout", + Canvas = "canvas", + JS = "js", } const standardSiderItems = [ @@ -268,6 +273,14 @@ const standardSiderItems = [ key: SiderKey.Setting, icon: , }, + { + key: SiderKey.Canvas, + icon: , + }, + { + key: SiderKey.JS, + icon: , + }, { key: SiderKey.Layout, icon: , @@ -536,40 +549,59 @@ function EditorView(props: EditorViewProps) { {panelStatus.left && editorModeStatus !== "layout" && ( {menuKey === SiderKey.State && } - {menuKey === SiderKey.Setting && ( - - - {application && - !isAggregationApp( - AppUILayoutType[application.applicationType] - ) && ( - <> - {appSettingsComp.getPropertyView()} - - - )} - {trans("leftPanel.toolbarTitle")} - {props.preloadComp.getPropertyView()} - dispatch( - setEditorExternalStateAction({ - showScriptsAndStyleModal: true, - }) - )} - > - - {trans("leftPanel.toolbarPreload")} - - - + + <> + {menuKey === SiderKey.Setting && ( + + + {application && + !isAggregationApp( + AppUILayoutType[application.applicationType] + ) && ( + <> + {appSettingsComp.getPropertyView()} + + )} + + + )} + {menuKey === SiderKey.Canvas && ( + + + {application && + !isAggregationApp( + AppUILayoutType[application.applicationType] + ) && ( + <> + {appSettingsComp.getPropertyView()} + + )} + + + )} + + + {menuKey === SiderKey.JS && ( + <> + {trans("leftPanel.toolbarTitle")} + {props.preloadComp.getPropertyView()} + dispatch( + setEditorExternalStateAction({ + showScriptsAndStyleModal: true, + }) + )} + > + + {trans("leftPanel.toolbarPreload")} + + {props.preloadComp.getJSLibraryPropertyView()} - + )} - {menuKey === SiderKey.Layout && ( )} - )} diff --git a/client/packages/lowcoder/src/pages/setting/theme/detail/index.tsx b/client/packages/lowcoder/src/pages/setting/theme/detail/index.tsx index 8013a1b7c..a6e66f5e7 100644 --- a/client/packages/lowcoder/src/pages/setting/theme/detail/index.tsx +++ b/client/packages/lowcoder/src/pages/setting/theme/detail/index.tsx @@ -174,7 +174,7 @@ class ThemeDetailPage extends React.Component + + + +

{trans("theme.canvas")}

+
+ + + ( + <> + {item.title && ( + + {item.title} + + )} + {item.items.map((canvasSettingItem) => ( + + + {canvasSettingItem.type == "gridColumns" && + { + this.configChange(params); + }} + /> + } + {canvasSettingItem.type == "gridRowHeight" && + { + this.configChange(params); + }} + /> + } + {canvasSettingItem.type == "gridRowCount" && + { + this.configChange(params); + }} + /> + } + {canvasSettingItem.type == "gridPaddingX" && + { + this.configChange(params); + }} + /> + } + {canvasSettingItem.type == "gridPaddingY" && + { + this.configChange(params); + }} + /> + } + {canvasSettingItem.type == "canvas" && + { + this.configChange(params); + }} + /> + } + {canvasSettingItem.type == "gridBgImage" && + { + this.configChange(params); + }} + /> + } + {canvasSettingItem.type == "gridBgImageRepeat" && + { + this.configChange(params); + }} + /> + } + {canvasSettingItem.type == "gridBgImageSize" && + { + this.configChange(params); + }} + /> + } + {canvasSettingItem.type == "gridBgImagePosition" && + { + this.configChange(params); + }} + /> + } + {canvasSettingItem.type == "gridBgImageOrigin" && + { + this.configChange(params); + }} + /> + } + + + ))} + + )} + /> + + + + +
@@ -538,23 +762,12 @@ class ThemeDetailPage extends React.Component } - {layoutSettingsItem.type == "gridColumns" && - { - this.configChange(params); - }} - /> - } {layoutSettingsItem.type == "showComponentLoadingIndicators" && { - console.log('configChange', params); this.configChange(params); }} /> diff --git a/client/packages/lowcoder/src/pages/setting/theme/detail/previewDsl.ts b/client/packages/lowcoder/src/pages/setting/theme/detail/previewDsl.ts index a868cca8f..ffeb03feb 100644 --- a/client/packages/lowcoder/src/pages/setting/theme/detail/previewDsl.ts +++ b/client/packages/lowcoder/src/pages/setting/theme/detail/previewDsl.ts @@ -677,6 +677,7 @@ const dsl = { settings: { maxWidth: { dropdown: "3200", input: "3200" }, themeId: "", + preventStylesOverwriting: false, }, preload: { libs: [], script: "", css: "" }, }; diff --git a/client/packages/lowcoder/src/util/hooks.ts b/client/packages/lowcoder/src/util/hooks.ts index 3bc5a2ed6..57fa2baa4 100644 --- a/client/packages/lowcoder/src/util/hooks.ts +++ b/client/packages/lowcoder/src/util/hooks.ts @@ -19,9 +19,13 @@ import { getDataSourceStructures } from "redux/selectors/datasourceSelectors"; import { DatasourceStructure } from "api/datasourceApi"; import { loadAuthSearchParams } from "pages/userAuth/authUtils"; import { ThemeContext } from "@lowcoder-ee/comps/utils/themeContext"; +import { defaultTheme } from "constants/themeConstants"; import { CompTypeContext } from "@lowcoder-ee/comps/utils/compTypeContext"; import { setInitialCompStyles } from "@lowcoder-ee/comps/utils/themeUtil"; import { CompAction, changeChildAction } from "lowcoder-core"; +import { ThemeDetail } from "@lowcoder-ee/api/commonSettingApi"; +import { uniq } from "lodash"; +import { constantColors } from "components/colorSelect/colorUtils"; export const ForceViewModeContext = React.createContext(false); @@ -168,7 +172,6 @@ export function useMetaData(datasourceId: string) { ); } - export function useMergeCompStyles( props: Record, dispatch: (action: CompAction) => void @@ -226,3 +229,31 @@ export function useMergeCompStyles( preventStyleOverwriting, ]); } + +type ColorKey = 'primary' | 'textDark' | 'textLight' | 'canvas' | 'primarySurface' | 'border'; +type ColorKeys = ColorKey[]; + +export function useThemeColors(allowGradient?: boolean) { + const currentTheme = useContext(ThemeContext)?.theme ?? {} as ThemeDetail; + const colorKeys: ColorKeys = ['primary', 'textDark', 'textLight', 'canvas', 'primarySurface', 'border']; + + return useMemo(() => { + let colors: string[] = []; + + colorKeys.forEach(colorKey => { + if (Boolean(defaultTheme[colorKey])) { + colors.push(defaultTheme[colorKey] ?? ''); + } + if (Boolean(currentTheme[colorKey])) { + colors.push(currentTheme[colorKey] ?? ''); + } + }) + if (!allowGradient) { + colors = colors.concat(constantColors); + } + return uniq(colors); + }, [ + currentTheme, + defaultTheme, + ]); +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/util/styleUtils.tsx b/client/packages/lowcoder/src/util/styleUtils.tsx new file mode 100644 index 000000000..e59a8fc25 --- /dev/null +++ b/client/packages/lowcoder/src/util/styleUtils.tsx @@ -0,0 +1,32 @@ +import { isValidColor, isValidGradient } from "components/colorSelect/colorUtils" +import { css } from "styled-components"; + +const getBackgroundStyle = (style: Record) => { + return css` + ${isValidColor(style.background) && `background-color: ${style.background}`}; + ${isValidGradient(style.background) && !Boolean(style.backgroundImage) && `background-image: ${style.background}`}; + ${!isValidGradient(style.background) && Boolean(style.backgroundImage) && `background-image: ${style.backgroundImage}`}; + ${isValidGradient(style.background) && Boolean(style.backgroundImage) && `background-image: url(${style.backgroundImage}), ${style.background}`}; + + ${style.backgroundImageRepeat && `background-repeat: ${style.backgroundImageRepeat};`}; + ${style.backgroundImageSize && `background-size: ${style.backgroundImageSize};`}; + ${style.backgroundImageOrigin && `background-origin: ${style.backgroundImageOrigin};`}; + ${style.backgroundImagePosition && `background-position: ${style.backgroundImagePosition};`}; + `; +} + +const getTextStyle = (color?: string) => { + return css` + ${isValidColor(color) && `color: ${color};`} + ${isValidGradient(color) && ` + background-image: -webkit-${color}; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + `} + `; +} + +export { + getBackgroundStyle, + getTextStyle, +} \ No newline at end of file diff --git a/client/packages/lowcoder/vite.config.mts b/client/packages/lowcoder/vite.config.mts index e6618c6ee..1be2ebfd8 100644 --- a/client/packages/lowcoder/vite.config.mts +++ b/client/packages/lowcoder/vite.config.mts @@ -102,9 +102,9 @@ export const viteConfig: UserConfig = { preprocessorOptions: { less: { modifyVars: { - "@primary-color": "#3377FF", + "@primary-color": "#b480de", "@link-color": "#3377FF", - "@border-color-base": "#D7D9E0", + "@border-color-base": "#b480de", "@border-radius-base": "4px", }, javascriptEnabled: true, diff --git a/client/yarn.lock b/client/yarn.lock index f3f30d79d..7825894fc 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -6655,6 +6655,13 @@ __metadata: languageName: node linkType: hard +"base64-arraybuffer@npm:^1.0.2": + version: 1.0.2 + resolution: "base64-arraybuffer@npm:1.0.2" + checksum: 15e6400d2d028bf18be4ed97702b11418f8f8779fb8c743251c863b726638d52f69571d4cc1843224da7838abef0949c670bde46936663c45ad078e89fee5c62 + languageName: node + linkType: hard + "base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -7907,6 +7914,15 @@ coolshapes-react@lowcoder-org/coolshapes-react: languageName: node linkType: hard +"css-line-break@npm:^2.1.0": + version: 2.1.0 + resolution: "css-line-break@npm:2.1.0" + dependencies: + utrie: ^1.0.2 + checksum: 37b1fe632b03be7a287cd394cef8b5285666343443125c510df9cfb6a4734a2c71e154ec8f7bbff72d7c339e1e5872989b1c52d86162aed27d6cc114725bb4d0 + languageName: node + linkType: hard + "css-loader@npm:^6.10.0": version: 6.11.0 resolution: "css-loader@npm:6.11.0" @@ -11285,6 +11301,16 @@ coolshapes-react@lowcoder-org/coolshapes-react: languageName: node linkType: hard +"html2canvas@npm:^1.4.1": + version: 1.4.1 + resolution: "html2canvas@npm:1.4.1" + dependencies: + css-line-break: ^2.1.0 + text-segmentation: ^1.0.3 + checksum: c134324af57f3262eecf982e436a4843fded3c6cf61954440ffd682527e4dd350e0c2fafd217c0b6f9a455fe345d0c67b4505689796ab160d4ca7c91c3766739 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -13598,6 +13624,13 @@ coolshapes-react@lowcoder-org/coolshapes-react: languageName: node linkType: hard +"lodash.throttle@npm:^4.1.1": + version: 4.1.1 + resolution: "lodash.throttle@npm:4.1.1" + checksum: 129c0a28cee48b348aef146f638ef8a8b197944d4e9ec26c1890c19d9bf5a5690fe11b655c77a4551268819b32d27f4206343e30c78961f60b561b8608c8c805 + languageName: node + linkType: hard + "lodash@npm:^3.9.1": version: 3.10.1 resolution: "lodash@npm:3.10.1" @@ -14000,6 +14033,7 @@ coolshapes-react@lowcoder-org/coolshapes-react: qrcode.react: ^3.1.0 rc-trigger: ^5.3.1 react: ^18.2.0 + react-best-gradient-color-picker: ^3.0.10 react-colorful: ^5.5.1 react-documents: ^1.2.1 react-dom: ^18.2.0 @@ -17485,6 +17519,24 @@ coolshapes-react@lowcoder-org/coolshapes-react: languageName: node linkType: hard +"react-best-gradient-color-picker@npm:^3.0.10": + version: 3.0.10 + resolution: "react-best-gradient-color-picker@npm:3.0.10" + dependencies: + html2canvas: ^1.4.1 + lodash.throttle: ^4.1.1 + tinycolor2: 1.4.2 + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 3f821beb164e7eed0c3247af99e492dc08bfa64b47f8d7f71d1543e5afbe1d9f01cde4a2917c9d794012dd1efbfe848ee58621b4f37801ea5655df8be806ea55 + languageName: node + linkType: hard + "react-colorful@npm:^5.5.1": version: 5.6.1 resolution: "react-colorful@npm:5.6.1" @@ -20198,6 +20250,15 @@ coolshapes-react@lowcoder-org/coolshapes-react: languageName: node linkType: hard +"text-segmentation@npm:^1.0.3": + version: 1.0.3 + resolution: "text-segmentation@npm:1.0.3" + dependencies: + utrie: ^1.0.2 + checksum: 2e24632d59567c55ab49ac324815e2f7a8043e63e26b109636322ac3e30692cee8679a448fd5d0f0598a345f407afd0e34ba612e22524cf576d382d84058c013 + languageName: node + linkType: hard + "text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" @@ -20276,6 +20337,13 @@ coolshapes-react@lowcoder-org/coolshapes-react: languageName: node linkType: hard +"tinycolor2@npm:1.4.2": + version: 1.4.2 + resolution: "tinycolor2@npm:1.4.2" + checksum: 57ed262e08815a4ab0ed933edafdbc6555a17081781766149813b44a080ecbe58b3ee281e81c0e75b42e4d41679f138cfa98eabf043f829e0683c04adb12c031 + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -21217,6 +21285,15 @@ coolshapes-react@lowcoder-org/coolshapes-react: languageName: node linkType: hard +"utrie@npm:^1.0.2": + version: 1.0.2 + resolution: "utrie@npm:1.0.2" + dependencies: + base64-arraybuffer: ^1.0.2 + checksum: c96fbb7d4d8855a154327da0b18e39b7511cc70a7e4bcc3658e24f424bb884312d72b5ba777500b8858e34d365dc6b1a921dc5ca2f0d341182519c6b78e280a5 + languageName: node + linkType: hard + "uuid@npm:^3.3.2": version: 3.4.0 resolution: "uuid@npm:3.4.0" diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationHistorySnapshotTS.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationHistorySnapshotTS.java new file mode 100644 index 000000000..1b6d86502 --- /dev/null +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/ApplicationHistorySnapshotTS.java @@ -0,0 +1,23 @@ +package org.lowcoder.domain.application.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.lowcoder.sdk.models.HasIdAndAuditing; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.Map; + +@ToString(callSuper = true) +@Document +@Getter +@Setter +@NoArgsConstructor +public class ApplicationHistorySnapshotTS extends HasIdAndAuditing { + + private String applicationId; + private Map dsl; + private Map context; + +} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistoryArchivedSnapshotRepository.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistoryArchivedSnapshotRepository.java new file mode 100644 index 000000000..dded29c35 --- /dev/null +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistoryArchivedSnapshotRepository.java @@ -0,0 +1,26 @@ +package org.lowcoder.domain.application.repository; + +import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Instant; + +@Repository +public interface ApplicationHistoryArchivedSnapshotRepository extends ReactiveMongoRepository { + + @Query(value = "{ 'applicationId': ?0, $and: [" + + "{$or: [ { 'context.operations': { $elemMatch: { 'compName': ?1 } } }, { $expr: { $eq: [?1, null] } } ]}, " + + "{$or: [ { 'dsl.settings.themeId': ?2 }, { $expr: { $eq: [?2, null] } } ] }, " + + "{$or: [ { 'createdAt': { $gte: ?3} }, { $expr: { $eq: [?3, null] } } ] }, " + + "{$or: [ { 'createdAt': { $lte: ?4} }, { $expr: { $eq: [?4, null] } } ] } " + + "]}", + fields = "{applicationId : 1, context: 1, createdBy : 1, createdAt : 1}") + Flux findAllByApplicationId(String applicationId, String compName, String theme, Instant createdAtFrom, Instant createdAtTo, Pageable pageable); + + Mono countByApplicationId(String applicationId); +} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java index a8e9f3d02..809decfd6 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java @@ -1,19 +1,26 @@ package org.lowcoder.domain.application.repository; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.stereotype.Repository; - import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.time.Instant; + @Repository -public interface ApplicationHistorySnapshotRepository extends ReactiveMongoRepository { +public interface ApplicationHistorySnapshotRepository extends ReactiveMongoRepository { - @Query(fields = "{applicationId : 1, context: 1, createdBy : 1, createdAt : 1}") - Flux findAllByApplicationId(String applicationId, Pageable pageable); + @Query(value = "{ 'applicationId': ?0, $and: [" + + "{$or: [ { 'context.operations': { $elemMatch: { 'compName': ?1 } } }, { $expr: { $eq: [?1, null] } } ]}, " + + "{$or: [ { 'dsl.settings.themeId': ?2 }, { $expr: { $eq: [?2, null] } } ] }, " + + "{$or: [ { 'createdAt': { $gte: ?3} }, { $expr: { $eq: [?3, null] } } ] }, " + + "{$or: [ { 'createdAt': { $lte: ?4} }, { $expr: { $eq: [?4, null] } } ] } " + + "]}", + fields = "{applicationId : 1, context: 1, createdBy : 1, createdAt : 1}") + Flux findAllByApplicationId(String applicationId, String compName, String theme, Instant createdAtFrom, Instant createdAtTo, Pageable pageable); Mono countByApplicationId(String applicationId); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java index a8845dd07..fd4a79f82 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java @@ -1,22 +1,24 @@ package org.lowcoder.domain.application.service; -import java.util.List; -import java.util.Map; - import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; import org.springframework.data.domain.PageRequest; - import reactor.core.publisher.Mono; +import java.time.Instant; +import java.util.List; +import java.util.Map; + public interface ApplicationHistorySnapshotService { Mono createHistorySnapshot(String applicationId, Map dsl, Map context, String userId); - Mono> listAllHistorySnapshotBriefInfo(String applicationId, PageRequest pageRequest); + Mono> listAllHistorySnapshotBriefInfo(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest); + Mono> listAllHistorySnapshotBriefInfoArchived(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest); Mono countByApplicationId(String applicationId); - Mono getHistorySnapshotDetail(String historySnapshotId); + Mono getHistorySnapshotDetail(String historySnapshotId); - Mono getLastSnapshotByApp(String applicationId); + Mono getHistorySnapshotDetailArchived(String historySnapshotId); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java index 25a140c24..c47b39955 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java @@ -1,46 +1,53 @@ package org.lowcoder.domain.application.service.impl; -import static org.lowcoder.sdk.exception.BizError.INVALID_HISTORY_SNAPSHOT; -import static org.lowcoder.sdk.util.ExceptionUtils.deferredError; -import static org.lowcoder.sdk.util.ExceptionUtils.ofException; - -import java.time.Instant; -import java.util.List; -import java.util.Map; - import lombok.RequiredArgsConstructor; import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; +import org.lowcoder.domain.application.repository.ApplicationHistoryArchivedSnapshotRepository; import org.lowcoder.domain.application.repository.ApplicationHistorySnapshotRepository; import org.lowcoder.domain.application.service.ApplicationHistorySnapshotService; import org.lowcoder.sdk.exception.BizError; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort.Direction; import org.springframework.stereotype.Service; - import reactor.core.publisher.Mono; +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import static org.lowcoder.sdk.exception.BizError.INVALID_HISTORY_SNAPSHOT; +import static org.lowcoder.sdk.util.ExceptionUtils.deferredError; +import static org.lowcoder.sdk.util.ExceptionUtils.ofException; + @RequiredArgsConstructor @Service public class ApplicationHistorySnapshotServiceImpl implements ApplicationHistorySnapshotService { private final ApplicationHistorySnapshotRepository repository; + private final ApplicationHistoryArchivedSnapshotRepository repositoryArchived; @Override public Mono createHistorySnapshot(String applicationId, Map dsl, Map context, String userId) { - ApplicationHistorySnapshot applicationHistorySnapshot = new ApplicationHistorySnapshot(); - applicationHistorySnapshot.setApplicationId(applicationId); - applicationHistorySnapshot.setDsl(dsl); - applicationHistorySnapshot.setContext(context); - return repository.save(applicationHistorySnapshot) + ApplicationHistorySnapshotTS applicationHistorySnapshotTS = new ApplicationHistorySnapshotTS(); + applicationHistorySnapshotTS.setApplicationId(applicationId); + applicationHistorySnapshotTS.setDsl(dsl); + applicationHistorySnapshotTS.setContext(context); + return repository.save(applicationHistorySnapshotTS) .thenReturn(true) .onErrorReturn(false); } @Override - public Mono> listAllHistorySnapshotBriefInfo(String applicationId, PageRequest pageRequest) { - return repository.findAllByApplicationId(applicationId, pageRequest.withSort(Direction.DESC, "id")) + public Mono> listAllHistorySnapshotBriefInfo(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest) { + return repository.findAllByApplicationId(applicationId, compName, theme, from, to, pageRequest.withSort(Direction.DESC, "id")) + .collectList() + .onErrorMap(Exception.class, e -> ofException(BizError.FETCH_HISTORY_SNAPSHOT_FAILURE, "FETCH_HISTORY_SNAPSHOT_FAILURE")); + } + + @Override + public Mono> listAllHistorySnapshotBriefInfoArchived(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest) { + return repositoryArchived.findAllByApplicationId(applicationId, compName, theme, from, to, pageRequest.withSort(Direction.DESC, "id")) .collectList() .onErrorMap(Exception.class, e -> ofException(BizError.FETCH_HISTORY_SNAPSHOT_FAILURE, "FETCH_HISTORY_SNAPSHOT_FAILURE")); } @@ -54,17 +61,15 @@ public Mono countByApplicationId(String applicationId) { @Override - public Mono getHistorySnapshotDetail(String historySnapshotId) { + public Mono getHistorySnapshotDetail(String historySnapshotId) { return repository.findById(historySnapshotId) .switchIfEmpty(deferredError(INVALID_HISTORY_SNAPSHOT, "INVALID_HISTORY_SNAPSHOT", historySnapshotId)); } + @Override - public Mono getLastSnapshotByApp(String applicationId) { - ApplicationHistorySnapshot _default = new ApplicationHistorySnapshot(); - _default.setCreatedAt(Instant.ofEpochMilli(0)); - _default.setCreatedBy(""); - return repository.findAllByApplicationId(applicationId, PageRequest.of(0, 1).withSort(Direction.DESC, "createdAt")) - .switchIfEmpty(Mono.just(_default)).next(); + public Mono getHistorySnapshotDetailArchived(String historySnapshotId) { + return repositoryArchived.findById(historySnapshotId) + .switchIfEmpty(deferredError(INVALID_HISTORY_SNAPSHOT, "INVALID_HISTORY_SNAPSHOT", historySnapshotId)); } } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationServiceImpl.java index 65fdeb8b2..99a541274 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/authentication/AuthenticationServiceImpl.java @@ -53,15 +53,15 @@ private Mono findAuthConfig(String orgId, Function findAllAuthConfigs(String orgId, boolean enableOnly) { Mono emailAuthConfigMono = orgMemberService.doesAtleastOneAdminExist() - .map(doesAtleastOneAdminExist -> { + .flatMap(doesAtleastOneAdminExist -> { boolean shouldEnableRegister; if(doesAtleastOneAdminExist) { shouldEnableRegister = authProperties.getEmail().getEnableRegister(); } else { shouldEnableRegister = Boolean.TRUE; } - return new FindAuthConfig - (new EmailAuthConfig(AuthSourceConstants.EMAIL, authProperties.getEmail().isEnable(), shouldEnableRegister), null); + if(orgId == null) return Mono.just(new FindAuthConfig(new EmailAuthConfig(AuthSourceConstants.EMAIL, authProperties.getEmail().isEnable(), shouldEnableRegister), null)); + else return organizationService.getById(orgId).map(organization -> new FindAuthConfig(new EmailAuthConfig(AuthSourceConstants.EMAIL, !organization.getIsEmailDisabled(), shouldEnableRegister), null)); }); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java index 18d7d2423..80d338b48 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java @@ -51,6 +51,13 @@ public class Organization extends HasIdAndAuditing implements BeforeMongodbWrite private String contactPhoneNumber; + private Boolean isEmailDisabled; + + public Boolean getIsEmailDisabled() { + if(isEmailDisabled == null) return false; + else return isEmailDisabled; + } + @JsonIgnore private String logoAssetId; diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java index 19d1fde58..b7dc9326c 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java @@ -27,6 +27,7 @@ public interface UserService { Mono> getByIds(Collection ids); Mono findBySourceAndId(String connectionSource, String connectionSourceUuid); + Mono findByEmailDeep(String email); Mono saveProfilePhoto(Part filePart, User t2); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index 4ba0e9cd7..c1559b42d 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -112,6 +112,7 @@ public Mono findByName(String rawUuid) { return repository.findByName(rawUuid); } + @Override public Mono findByEmailDeep(String email) { if(StringUtils.isEmpty(email)) return Mono.empty(); return repository.findByEmailOrConnections_Email(email, email).next(); diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java index 011f321cf..b50d06935 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java @@ -160,6 +160,7 @@ public static class Marketplace { @Setter public static class Query { private long readStructureTimeout = 15000; + private long appSnapshotKeepDuration = 30; } @Data 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 2ed207aae..88d14e210 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiService.java @@ -17,7 +17,7 @@ public interface ApplicationApiService { Mono create(ApplicationEndpoints.CreateApplicationRequest createApplicationRequest); - Flux getRecycledApplications(); + Flux getRecycledApplications(String name); Mono delete(String applicationId); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java index 1fceb04d5..25d772cdb 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationApiServiceImpl.java @@ -169,8 +169,8 @@ private Mono autoGrantPermissionsByFolderDefault(String applicationId, @Nu } @Override - public Flux getRecycledApplications() { - return userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(null, ApplicationStatus.RECYCLED, false); + public Flux getRecycledApplications(String name) { + return userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(null, ApplicationStatus.RECYCLED, false, name); } private Mono checkCurrentUserApplicationPermission(String applicationId, ResourceAction action) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java index ffd88b337..e4807183b 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 @@ -77,8 +77,8 @@ public Mono> restore(@PathVariable String applicationId) { } @Override - public Mono>> getRecycledApplications() { - return applicationApiService.getRecycledApplications() + public Mono>> getRecycledApplications(@RequestParam(required = false) String name) { + return applicationApiService.getRecycledApplications(name) .collectList() .map(ResponseView::success); } @@ -159,9 +159,10 @@ public Mono> getUserHomePage(@RequestParam(requir @Override public Mono>> getApplications(@RequestParam(required = false) Integer applicationType, @RequestParam(required = false) ApplicationStatus applicationStatus, - @RequestParam(defaultValue = "true") boolean withContainerSize) { + @RequestParam(defaultValue = "true") boolean withContainerSize, + @RequestParam(required = false) String name) { ApplicationType applicationTypeEnum = applicationType == null ? null : ApplicationType.fromValue(applicationType); - return userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(applicationTypeEnum, applicationStatus, withContainerSize) + return userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(applicationTypeEnum, applicationStatus, withContainerSize, name) .collectList() .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 0b29c1ebc..8db147302 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java @@ -71,7 +71,7 @@ public interface ApplicationEndpoints description = "List all the recycled Lowcoder Applications in the recycle bin where the authenticated or impersonated user has access." ) @GetMapping("/recycle/list") - public Mono>> getRecycledApplications(); + public Mono>> getRecycledApplications(@RequestParam(required = false) String name); @Operation( tags = TAG_APPLICATION_MANAGEMENT, @@ -165,7 +165,8 @@ public Mono> updateEditState(@PathVariable String applicat @GetMapping("/list") public Mono>> getApplications(@RequestParam(required = false) Integer applicationType, @RequestParam(required = false) ApplicationStatus applicationStatus, - @RequestParam(defaultValue = "true") boolean withContainerSize); + @RequestParam(defaultValue = "true") boolean withContainerSize, + @RequestParam(required = false) String name); @Operation( tags = TAG_APPLICATION_MANAGEMENT, diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java index f4c476e76..6b6d94a51 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java @@ -1,18 +1,14 @@ package org.lowcoder.api.application; -import static org.lowcoder.api.util.ViewBuilder.multiBuild; - -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - +import com.google.common.collect.ImmutableMap; +import lombok.RequiredArgsConstructor; import org.lowcoder.api.application.view.HistorySnapshotDslView; import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.api.home.SessionUserService; import org.lowcoder.api.util.Pagination; import org.lowcoder.domain.application.model.Application; import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; import org.lowcoder.domain.application.service.ApplicationHistorySnapshotService; import org.lowcoder.domain.application.service.ApplicationService; import org.lowcoder.domain.permission.model.ResourceAction; @@ -22,11 +18,14 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; -import com.google.common.collect.ImmutableMap; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import reactor.core.publisher.Mono; +import static org.lowcoder.api.util.ViewBuilder.multiBuild; @RequiredArgsConstructor @RestController @@ -55,15 +54,56 @@ public Mono> create(@RequestBody ApplicationHistorySnapsho @Override public Mono>> listAllHistorySnapshotBriefInfo(@PathVariable String applicationId, - @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam String compName, + @RequestParam String theme, + @RequestParam Instant from, + @RequestParam Instant to) { + + Pagination pagination = Pagination.of(page, size).check(); + + return sessionUserService.getVisitorId() + .delayUntil(visitor -> resourcePermissionService.checkResourcePermissionWithError(visitor, applicationId, + ResourceAction.EDIT_APPLICATIONS)) + .flatMap(__ -> applicationHistorySnapshotService.listAllHistorySnapshotBriefInfo(applicationId, compName, theme, from, to, pagination.toPageRequest())) + .flatMap(snapshotList -> { + Mono> snapshotBriefInfoList = multiBuild(snapshotList, + ApplicationHistorySnapshotTS::getCreatedBy, + userService::getByIds, + (applicationHistorySnapshotTS, user) -> new ApplicationHistorySnapshotBriefInfo( + applicationHistorySnapshotTS.getId(), + applicationHistorySnapshotTS.getContext(), + applicationHistorySnapshotTS.getCreatedBy(), + user.getName(), + user.getAvatarUrl(), + applicationHistorySnapshotTS.getCreatedAt().toEpochMilli() + ) + ); + + Mono applicationHistorySnapshotCount = applicationHistorySnapshotService.countByApplicationId(applicationId); + + return Mono.zip(snapshotBriefInfoList, applicationHistorySnapshotCount) + .map(tuple -> ImmutableMap.of("list", tuple.getT1(), "count", tuple.getT2())); + }) + .map(ResponseView::success); + } + + @Override + public Mono>> listAllHistorySnapshotBriefInfoArchived(@PathVariable String applicationId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam String compName, + @RequestParam String theme, + @RequestParam Instant from, + @RequestParam Instant to) { Pagination pagination = Pagination.of(page, size).check(); return sessionUserService.getVisitorId() .delayUntil(visitor -> resourcePermissionService.checkResourcePermissionWithError(visitor, applicationId, ResourceAction.EDIT_APPLICATIONS)) - .flatMap(__ -> applicationHistorySnapshotService.listAllHistorySnapshotBriefInfo(applicationId, - pagination.toPageRequest())) + .flatMap(__ -> applicationHistorySnapshotService.listAllHistorySnapshotBriefInfoArchived(applicationId, compName, theme, from, to, pagination.toPageRequest())) .flatMap(snapshotList -> { Mono> snapshotBriefInfoList = multiBuild(snapshotList, ApplicationHistorySnapshot::getCreatedBy, @@ -93,8 +133,30 @@ public Mono> getHistorySnapshotDsl(@PathVar .delayUntil(visitor -> resourcePermissionService.checkResourcePermissionWithError(visitor, applicationId, ResourceAction.EDIT_APPLICATIONS)) .flatMap(__ -> applicationHistorySnapshotService.getHistorySnapshotDetail(snapshotId)) + .map(ApplicationHistorySnapshotTS::getDsl) + .zipWhen(applicationService::getAllDependentModulesFromDsl) + .map(tuple -> { + Map applicationDsl = tuple.getT1(); + List dependentModules = tuple.getT2(); + Map> dependentModuleDsl = dependentModules.stream() + .collect(Collectors.toMap(Application::getId, Application::getLiveApplicationDsl, (a, b) -> b)); + return HistorySnapshotDslView.builder() + .applicationsDsl(applicationDsl) + .moduleDSL(dependentModuleDsl) + .build(); + }) + .map(ResponseView::success); + } + + @Override + public Mono> getHistorySnapshotDslArchived(@PathVariable String applicationId, + @PathVariable String snapshotId) { + return sessionUserService.getVisitorId() + .delayUntil(visitor -> resourcePermissionService.checkResourcePermissionWithError(visitor, applicationId, + ResourceAction.EDIT_APPLICATIONS)) + .flatMap(__ -> applicationHistorySnapshotService.getHistorySnapshotDetailArchived(snapshotId)) .map(ApplicationHistorySnapshot::getDsl) - .zipWhen(dsl -> applicationService.getAllDependentModulesFromDsl(dsl)) + .zipWhen(applicationService::getAllDependentModulesFromDsl) .map(tuple -> { Map applicationDsl = tuple.getT1(); List dependentModules = tuple.getT2(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotEndpoints.java index 9867ab3d1..32276f3a8 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotEndpoints.java @@ -1,7 +1,9 @@ package org.lowcoder.api.application; +import java.time.Instant; import java.util.Map; +import jakarta.annotation.Nullable; import org.lowcoder.api.application.view.HistorySnapshotDslView; import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.infra.constant.NewUrl; @@ -39,7 +41,23 @@ public interface ApplicationHistorySnapshotEndpoints ) @GetMapping("/{applicationId}") public Mono>> listAllHistorySnapshotBriefInfo(@PathVariable String applicationId, - @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size); + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false ) @Nullable String compName, @RequestParam(required = false ) @Nullable String theme, + @RequestParam(required = false ) @Nullable Instant from, + @RequestParam(required = false ) @Nullable Instant to); + + @Operation( + tags = TAG_APPLICATION_HISTORY_MANAGEMENT, + operationId = "listApplicationSnapshotsArchived", + summary = "List Archived Application Snapshots", + description = "Retrieve a list of Archived Snapshots associated with a specific Application within Lowcoder." + ) + @GetMapping("/archive/{applicationId}") + public Mono>> listAllHistorySnapshotBriefInfoArchived(@PathVariable String applicationId, + @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false ) @Nullable String compName, @RequestParam(required = false ) @Nullable String theme, + @RequestParam(required = false ) @Nullable Instant from, + @RequestParam(required = false ) @Nullable Instant to); @Operation( tags = TAG_APPLICATION_HISTORY_MANAGEMENT, @@ -51,6 +69,16 @@ public Mono>> listAllHistorySnapshotBriefInfo(@ public Mono> getHistorySnapshotDsl(@PathVariable String applicationId, @PathVariable String snapshotId); + @Operation( + tags = TAG_APPLICATION_HISTORY_MANAGEMENT, + operationId = "getApplicationSnapshotArchived", + summary = "Retrieve Archived Application Snapshot", + description = "Retrieve a specific Archived Application Snapshot within Lowcoder using the Application and Snapshot IDs." + ) + @GetMapping("/archive/{applicationId}/{snapshotId}") + public Mono> getHistorySnapshotDslArchived(@PathVariable String applicationId, + @PathVariable String snapshotId); + public record ApplicationHistorySnapshotBriefInfo(String snapshotId, Map context, String userId, String userName, String userAvatar, long createTime) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java index 4b67de89c..7ca9fe3f9 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java @@ -246,9 +246,13 @@ public Mono enableAuthConfig(AuthConfigRequest authConfigRequest) { .then(sessionUserService.getVisitorOrgMemberCache()) .flatMap(orgMember -> organizationService.getById(orgMember.getOrgId())) .doOnNext(organization -> { - boolean duplicateAuthType = addOrUpdateNewAuthConfig(organization, authConfigFactory.build(authConfigRequest, true)); - if(duplicateAuthType) { - deferredError(DUPLICATE_AUTH_CONFIG_ADDITION, "DUPLICATE_AUTH_CONFIG_ADDITION"); + if(authConfigRequest.getId().equals("EMAIL")) { + organization.setIsEmailDisabled(false); + } else { + boolean duplicateAuthType = addOrUpdateNewAuthConfig(organization, authConfigFactory.build(authConfigRequest, true)); + if (duplicateAuthType) { + deferredError(DUPLICATE_AUTH_CONFIG_ADDITION, "DUPLICATE_AUTH_CONFIG_ADDITION"); + } } }) .flatMap(organization -> organizationService.update(organization.getId(), organization)); @@ -346,22 +350,15 @@ private Mono checkIfAdmin() { * If true, throw an exception to avoid disabling the last effective connection way. */ private Mono checkIfOnlyEffectiveCurrentUserConnections(String authId) { - Mono> userConnectionAuthConfigIdListMono = sessionUserService.getVisitor() - .flatMapIterable(User::getConnections) - .filter(connection -> StringUtils.isNotBlank(connection.getAuthId())) - .map(Connection::getAuthId) - .collectList(); - Mono> orgAuthIdListMono = authenticationService.findAllAuthConfigs(null, true) - .map(FindAuthConfig::authConfig) - .map(AbstractAuthConfig::getId) - .collectList(); - return Mono.zip(userConnectionAuthConfigIdListMono, orgAuthIdListMono) - .delayUntil(tuple -> { - List userConnectionAuthConfigIds = tuple.getT1(); - List orgAuthConfigIds = tuple.getT2(); - userConnectionAuthConfigIds.retainAll(orgAuthConfigIds); - userConnectionAuthConfigIds.remove(authId); - if (CollectionUtils.isEmpty(userConnectionAuthConfigIds)) { + return sessionUserService.getVisitorOrgMemberCache() + .map(OrgMember::getOrgId) + .flatMap(orgId -> authenticationService.findAllAuthConfigs(orgId, true) + .map(FindAuthConfig::authConfig) + .map(AbstractAuthConfig::getId) + .collectList()) + .delayUntil(orgAuthConfigIds -> { + orgAuthConfigIds.remove(authId); + if (CollectionUtils.isEmpty(orgAuthConfigIds)) { return Mono.error(new BizException(DISABLE_AUTH_CONFIG_FORBIDDEN, "DISABLE_AUTH_CONFIG_FORBIDDEN")); } return Mono.empty(); @@ -370,26 +367,29 @@ private Mono checkIfOnlyEffectiveCurrentUserConnections(String authId) { } private void disableAuthConfig(Organization organization, String authId, boolean delete) { - - Predicate authConfigPredicate = abstractAuthConfig -> Objects.equals(abstractAuthConfig.getId(), authId); - - if(delete) { - List abstractAuthConfigs = Optional.of(organization) - .map(Organization::getAuthConfigs) - .orElse(Collections.emptyList()); - - abstractAuthConfigs.removeIf(authConfigPredicate); - - organization.getOrganizationDomain().setConfigs(abstractAuthConfigs); - + if(authId.equals("EMAIL")) { + organization.setIsEmailDisabled(true); } else { - Optional.of(organization) - .map(Organization::getAuthConfigs) - .orElse(Collections.emptyList()).stream() - .filter(authConfigPredicate) - .forEach(abstractAuthConfig -> { - abstractAuthConfig.setEnable(false); - }); + Predicate authConfigPredicate = abstractAuthConfig -> Objects.equals(abstractAuthConfig.getId(), authId); + + if (delete) { + List abstractAuthConfigs = Optional.of(organization) + .map(Organization::getAuthConfigs) + .orElse(Collections.emptyList()); + + abstractAuthConfigs.removeIf(authConfigPredicate); + + organization.getOrganizationDomain().setConfigs(abstractAuthConfigs); + + } else { + Optional.of(organization) + .map(Organization::getAuthConfigs) + .orElse(Collections.emptyList()).stream() + .filter(authConfigPredicate) + .forEach(abstractAuthConfig -> { + abstractAuthConfig.setEnable(false); + }); + } } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceApiService.java index f1331bd01..4df2cd16a 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceApiService.java @@ -6,6 +6,7 @@ import org.lowcoder.domain.permission.model.ResourceRole; import org.lowcoder.domain.plugin.client.dto.GetPluginDynamicConfigRequestDTO; import org.lowcoder.sdk.models.DatasourceTestResult; +import org.springframework.web.bind.annotation.RequestParam; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -15,11 +16,11 @@ public interface DatasourceApiService { Mono create(Datasource datasource); - Flux listJsDatasourcePlugins(String applicationId); + Flux listJsDatasourcePlugins(String applicationId, String name, String type); - Flux listAppDataSources(String appId); + Flux listAppDataSources(String appId, String name, String type); - Flux listOrgDataSources(String orgId); + Flux listOrgDataSources(String orgId, String name, String type); Mono update(String datasourceId, Datasource updatedDatasource); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceApiServiceImpl.java index 716c22892..16758884a 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceApiServiceImpl.java @@ -84,27 +84,33 @@ public Mono create(Datasource datasource) { } @Override - public Flux listJsDatasourcePlugins(String applicationId) { + public Flux listJsDatasourcePlugins(String applicationId, String name, String type) { return applicationService.findById(applicationId) .delayUntil(application -> applicationApiService.checkPermissionWithReadableErrorMsg(applicationId, READ_APPLICATIONS)) .flatMapMany(application -> datasourceService.getByOrgId(application.getOrganizationId())) .filter(datasource -> datasource.getDatasourceStatus() == DatasourceStatus.NORMAL) - .filter(datasource -> datasourceMetaInfoService.isJsDatasourcePlugin(datasource.getType())) - .delayUntil(datasource -> jsDatasourceHelper.processDynamicQueryConfig(datasource)) + .filter(datasource -> datasourceMetaInfoService.isJsDatasourcePlugin(datasource.getType()) && + (name == null || StringUtils.containsIgnoreCase(datasource.getName(), name)) && + (type == null || datasource.getType().equals(type)) + ) + .delayUntil(jsDatasourceHelper::processDynamicQueryConfig) .doOnNext(datasource -> datasource.setDetailConfig(null)); } @Override - public Flux listAppDataSources(String appId) { + public Flux listAppDataSources(String appId, String name, String type) { return applicationService.findById(appId) - .flatMapMany(application -> listOrgDataSources(application.getOrganizationId())); + .flatMapMany(application -> listOrgDataSources(application.getOrganizationId(), name, type)); } @Override - public Flux listOrgDataSources(String orgId) { + public Flux listOrgDataSources(String orgId, String name, String type) { // get datasource Flux datasourceFlux = datasourceService.getByOrgId(orgId) - .filter(datasource -> datasource.getDatasourceStatus() == DatasourceStatus.NORMAL) + .filter(datasource -> datasource.getDatasourceStatus() == DatasourceStatus.NORMAL && + (name == null || StringUtils.containsIgnoreCase(datasource.getName(), name)) && + (type == null || datasource.getType().equals(type)) + ) .cache(); // get user-datasource permissions diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java index 96cc761f9..f604a287b 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java @@ -116,9 +116,9 @@ public Mono> getStructure(@PathVariable String * name, type... and the plugin definition of it, excluding the detail configs such as the connection uri, password... */ @Override - public Mono>> listJsDatasourcePlugins(@RequestParam("appId") String applicationId) { + public Mono>> listJsDatasourcePlugins(@RequestParam("appId") String applicationId, @RequestParam(required = false) String name, @RequestParam(required = false) String type) { String objectId = gidService.convertApplicationIdToObjectId(applicationId); - return datasourceApiService.listJsDatasourcePlugins(objectId) + return datasourceApiService.listJsDatasourcePlugins(objectId, name, type) .collectList() .map(ResponseView::success); } @@ -139,24 +139,24 @@ public Mono>> getPluginDynamicConfig( @SneakyThrows @Override - public Mono>> listOrgDataSources(@RequestParam(name = "orgId") String orgId) { + public Mono>> listOrgDataSources(@RequestParam(name = "orgId") String orgId, @RequestParam(required = false) String name, @RequestParam(required = false) String type) { if (StringUtils.isBlank(orgId)) { return ofError(BizError.INVALID_PARAMETER, "ORG_ID_EMPTY"); } String objectId = gidService.convertOrganizationIdToObjectId(orgId); - return datasourceApiService.listOrgDataSources(objectId) + return datasourceApiService.listOrgDataSources(objectId, name, type) .collectList() .map(ResponseView::success); } @Override - public Mono>> listAppDataSources(@RequestParam(name = "appId") String applicationId) { + public Mono>> listAppDataSources(@RequestParam(name = "appId") String applicationId, @RequestParam(required = false) String name, @RequestParam(required = false) String type) { if (StringUtils.isBlank(applicationId)) { return ofError(BizError.INVALID_PARAMETER, "INVALID_APP_ID"); } String objectId = gidService.convertApplicationIdToObjectId(applicationId); - return datasourceApiService.listAppDataSources(objectId) + return datasourceApiService.listAppDataSources(objectId, name, type) .collectList() .map(ResponseView::success); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java index ec9c8512c..bffadd018 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java @@ -99,7 +99,7 @@ public Mono> getStructure(@PathVariable String description = "Retrieve a list of node service plugins available within Lowcoder." ) @GetMapping("/jsDatasourcePlugins") - public Mono>> listJsDatasourcePlugins(@RequestParam("appId") String applicationId); + public Mono>> listJsDatasourcePlugins(@RequestParam("appId") String applicationId, @RequestParam(required = false) String name, @RequestParam(required = false) String type); /** * Proxy the request to the node service, besides, add the "extra" information from the data source config stored in the mongodb if exists to @@ -123,7 +123,7 @@ public Mono>> getPluginDynamicConfig( ) @JsonView(JsonViews.Public.class) @GetMapping("/listByOrg") - public Mono>> listOrgDataSources(@RequestParam(name = "orgId") String orgId); + public Mono>> listOrgDataSources(@RequestParam(name = "orgId") String orgId, @RequestParam String name, @RequestParam String type); @Operation( tags = TAG_DATASOURCE_MANAGEMENT, @@ -134,7 +134,7 @@ public Mono>> getPluginDynamicConfig( @Deprecated @JsonView(JsonViews.Public.class) @GetMapping("/listByApp") - public Mono>> listAppDataSources(@RequestParam(name = "appId") String applicationId); + public Mono>> listAppDataSources(@RequestParam(name = "appId") String applicationId, @RequestParam String name, @RequestParam String type); @Operation( tags = TAG_DATASOURCE_PERMISSIONS, diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java index c7feaae0f..92dd32b73 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java @@ -1,28 +1,18 @@ package org.lowcoder.api.framework.configuration; -import java.util.ArrayList; - import org.lowcoder.api.framework.plugin.LowcoderPluginManager; import org.lowcoder.api.framework.plugin.endpoint.PluginEndpointHandler; -// Falk: eventually not needed -import org.lowcoder.api.framework.plugin.security.PluginAuthorizationManager; -import org.lowcoder.plugin.api.EndpointExtension; -import org.springframework.aop.Advisor; -import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; -import org.springframework.context.annotation.Role; -import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder; -import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; - import reactor.core.publisher.Mono; +import java.util.ArrayList; + @Configuration public class PluginConfiguration { @@ -43,15 +33,4 @@ RouterFunction pluginEndpoints(LowcoderPluginManager pluginManager, PluginEnd return (endpoints == null) ? pluginsList : pluginsList.andOther(endpoints); } - - // Falk: eventually not needed - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - Advisor protectPluginEndpoints(PluginAuthorizationManager pluginAauthManager) - { - AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(EndpointExtension.class, true); - AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(pointcut, pluginAauthManager); - interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder() -1); - return interceptor; - } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/UserSessionPersistenceFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/UserSessionPersistenceFilter.java index 589aae782..75d28bd46 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/UserSessionPersistenceFilter.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/UserSessionPersistenceFilter.java @@ -122,6 +122,7 @@ private Mono refreshOauthToken(Triple> trip return authenticationService .findAllAuthConfigs(orgId, true) + .onErrorResume(e -> Flux.empty()) .filter(findAuthConfig -> findAuthConfig.authConfig().getId().equals(connection.getAuthId())) .switchIfEmpty(Mono.empty()) .flatMap(findAuthConfig -> { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java index 214252827..1e1b3c8e3 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java @@ -1,5 +1,6 @@ package org.lowcoder.api.framework.plugin.endpoint; +import static org.lowcoder.sdk.exception.BizError.NOT_AUTHORIZED; import static org.springframework.web.reactive.function.server.RequestPredicates.DELETE; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; import static org.springframework.web.reactive.function.server.RequestPredicates.OPTIONS; @@ -8,22 +9,30 @@ import static org.springframework.web.reactive.function.server.RequestPredicates.PUT; import static org.springframework.web.reactive.function.server.RouterFunctions.route; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.lowcoder.api.framework.plugin.data.PluginServerRequest; +import org.lowcoder.api.framework.plugin.security.PluginAuthorizationManager; import org.lowcoder.api.framework.plugin.security.SecuredEndpoint; import org.lowcoder.plugin.api.EndpointExtension; import org.lowcoder.plugin.api.PluginEndpoint; import org.lowcoder.plugin.api.data.EndpointRequest; import org.lowcoder.plugin.api.data.EndpointResponse; import org.lowcoder.sdk.exception.BaseException; +import org.lowcoder.sdk.exception.BizException; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactoryBean; +import org.springframework.aop.framework.ReflectiveMethodInvocation; import org.springframework.aop.target.SimpleBeanTargetSource; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; @@ -31,7 +40,11 @@ import org.springframework.core.ResolvableType; import org.springframework.http.ResponseCookie; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.RequestPredicate; import org.springframework.web.reactive.function.server.RouterFunction; @@ -52,6 +65,7 @@ public class PluginEndpointHandlerImpl implements PluginEndpointHandler private final ApplicationContext applicationContext; private final DefaultListableBeanFactory beanFactory; + private final PluginAuthorizationManager pluginAuthorizationManager; @Override public void registerEndpoints(String pluginUrlPrefix, List endpoints) @@ -101,26 +115,69 @@ private void registerEndpointHandler(String urlPrefix, PluginEndpoint endpoint, log.info("Registered endpoint: {} -> {}: {}", endpoint.getClass().getSimpleName(), endpointMeta.method(), urlPrefix + endpointMeta.uri()); } - - @SecuredEndpoint + public Mono runPluginEndpointMethod(PluginEndpoint endpoint, EndpointExtension endpointMeta, Method handler, ServerRequest request) { - Mono result = null; - try - { - log.info("Running plugin endpoint method {}\nRequest: {}", handler.getName(), request); + log.info("Running plugin endpoint method {}\nRequest: {}", handler.getName(), request); - EndpointResponse response = (EndpointResponse)handler.invoke(endpoint, PluginServerRequest.fromServerRequest(request)); - result = createServerResponse(response); - } - catch (IllegalAccessException | InvocationTargetException cause) - { - throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !"); - } - return result; + Mono monoAuthentication = ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication).cache(); + Mono decisionMono = monoAuthentication.flatMap(authentication -> { + MethodInvocation methodInvocation = null; + try { + methodInvocation = getMethodInvocation(endpointMeta, authentication); + } catch (NoSuchMethodException e) { + return Mono.error(new RuntimeException(e)); + } + return pluginAuthorizationManager.check(monoAuthentication, methodInvocation); + }); + + return decisionMono.handle((authorizationDecision, sink) -> { + if(!authorizationDecision.isGranted()) sink.error(new BizException(NOT_AUTHORIZED, "NOT_AUTHORIZED")); + try { + sink.next((EndpointResponse) handler.invoke(endpoint, PluginServerRequest.fromServerRequest(request))); + } catch (IllegalAccessException | InvocationTargetException e) { + sink.error(new RuntimeException(e)); + } + }).flatMap(this::createServerResponse); } - - + + private static @NotNull MethodInvocation getMethodInvocation(EndpointExtension endpointMeta, Authentication authentication) throws NoSuchMethodException { + Method method = Authentication.class.getMethod("isAuthenticated"); + Object[] arguments = new Object[]{"someString", endpointMeta}; + return new MethodInvocation() { + @NotNull + @Override + public Method getMethod() { + return method; + } + + @NotNull + @Override + public Object[] getArguments() { + return arguments; + } + + @Nullable + @Override + public Object proceed() throws Throwable { + return null; + } + + @Nullable + @Override + public Object getThis() { + return authentication; + } + + @NotNull + @Override + public AccessibleObject getStaticPart() { + return null; + } + }; + } + + private void registerRouterFunctionMapping(String endpointName, RouterFunction routerFunction) { String beanName = "pluginEndpoint_" + endpointName + "_" + System.currentTimeMillis(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java index 721fc015f..de161bb19 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java @@ -27,7 +27,7 @@ public interface FolderApiService { Mono upsertLastViewTime(@Nullable String folderId); - Flux getElements(@Nullable String folderId, @Nullable ApplicationType applicationType); + Flux getElements(@Nullable String folderId, @Nullable ApplicationType applicationType, @Nullable String name); Mono grantPermission(String folderId, Set userIds, Set groupIds, ResourceRole role); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiServiceImpl.java index 0517fa935..79fafda96 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiServiceImpl.java @@ -233,8 +233,8 @@ public Mono upsertLastViewTime(@Nullable String folderId) { * @return flux of {@link ApplicationInfoView} or {@link FolderInfoView} */ @Override - public Flux getElements(@Nullable String folderId, @Nullable ApplicationType applicationType) { - return buildApplicationInfoViewTree(applicationType) + public Flux getElements(@Nullable String folderId, @Nullable ApplicationType applicationType, @Nullable String name) { + return buildApplicationInfoViewTree(applicationType, name) .flatMap(tree -> { FolderNode folderNode = tree.get(folderId); if (folderNode == null) { @@ -278,13 +278,13 @@ private Mono> buildFolderTree(String orgId) { .map(folders -> new Tree<>(folders, Folder::getId, Folder::getParentFolderId, Collections.emptyList(), null, null)); } - private Mono> buildApplicationInfoViewTree(@Nullable ApplicationType applicationType) { + private Mono> buildApplicationInfoViewTree(@Nullable ApplicationType applicationType, @Nullable String name) { Mono orgMemberMono = sessionUserService.getVisitorOrgMemberCache() .cache(); Flux applicationInfoViewFlux = - userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(applicationType, ApplicationStatus.NORMAL, false) + userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(applicationType, ApplicationStatus.NORMAL, false, null) .cache(); Mono> application2FolderMapMono = applicationInfoViewFlux @@ -294,6 +294,9 @@ private Mono> buildApplicationInfoView .collectMap(FolderElement::elementId, FolderElement::folderId); Flux folderFlux = orgMemberMono.flatMapMany(orgMember -> folderService.findByOrganizationId(orgMember.getOrgId())) + .filter(folder -> name == null || StringUtils.containsIgnoreCase(folder.getName(), name) + || StringUtils.containsIgnoreCase(folder.getType(), name) + || StringUtils.containsIgnoreCase(folder.getDescription(), name)) .cache(); Mono> folderId2LastViewTimeMapMono = orgMemberMono diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java index f0be4d535..f75b73d9d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java @@ -69,9 +69,10 @@ public Mono> update(@RequestBody Folder folder) { */ @Override public Mono>> getElements(@RequestParam(value = "id", required = false) String folderId, - @RequestParam(value = "applicationType", required = false) ApplicationType applicationType) { + @RequestParam(value = "applicationType", required = false) ApplicationType applicationType, + @RequestParam(required = false) String name) { String objectId = gidService.convertFolderIdToObjectId(folderId); - return folderApiService.getElements(objectId, applicationType) + return folderApiService.getElements(objectId, applicationType, name) .collectList() .delayUntil(__ -> folderApiService.upsertLastViewTime(objectId)) .map(ResponseView::success); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java index 420bfca3a..337048d96 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java @@ -69,7 +69,8 @@ public interface FolderEndpoints ) @GetMapping("/elements") public Mono>> getElements(@RequestParam(value = "id", required = false) String folderId, - @RequestParam(value = "applicationType", required = false) ApplicationType applicationType); + @RequestParam(value = "applicationType", required = false) ApplicationType applicationType, + @RequestParam(required = false) String name); @Operation( tags = TAG_FOLDER_MANAGEMENT, diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiService.java index e89962605..e711304a4 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiService.java @@ -24,7 +24,7 @@ public interface UserHomeApiService { Mono getUserHomePageView(ApplicationType applicationType); Flux getAllAuthorisedApplications4CurrentOrgMember(@Nullable ApplicationType applicationType, - @Nullable ApplicationStatus applicationStatus, boolean withContainerSize); + @Nullable ApplicationStatus applicationStatus, boolean withContainerSize, @Nullable String name); Flux getAllAuthorisedBundles4CurrentOrgMember(@Nullable BundleStatus bundleStatus); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java index b125f82fc..ae5f22fcf 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/UserHomeApiServiceImpl.java @@ -157,7 +157,7 @@ public Mono getUserHomePageView(ApplicationType applicationTyp } return organizationService.getById(currentOrgId) - .zipWith(folderApiService.getElements(null, applicationType).collectList()) + .zipWith(folderApiService.getElements(null, applicationType, null).collectList()) .map(tuple2 -> { Organization organization = tuple2.getT1(); List list = tuple2.getT2(); @@ -189,7 +189,7 @@ public Mono getUserHomePageView(ApplicationType applicationTyp @Override public Flux getAllAuthorisedApplications4CurrentOrgMember(@Nullable ApplicationType applicationType, - @Nullable ApplicationStatus applicationStatus, boolean withContainerSize) { + @Nullable ApplicationStatus applicationStatus, boolean withContainerSize, @Nullable String name) { return sessionUserService.getVisitorOrgMemberCache() .flatMapMany(orgMember -> { @@ -202,8 +202,9 @@ public Flux getAllAuthorisedApplications4CurrentOrgMember(@ } return applicationService.findByOrganizationIdWithoutDsl(currentOrgId); }) - .filter(application -> isNull(applicationType) || application.getApplicationType() == applicationType.getValue()) - .filter(application -> isNull(applicationStatus) || application.getApplicationStatus() == applicationStatus) + .filter(application -> (isNull(applicationType) || application.getApplicationType() == applicationType.getValue()) + && (isNull(applicationStatus) || application.getApplicationStatus() == applicationStatus) + && (isNull(name) || StringUtils.containsIgnoreCase(application.getName(), name))) .cache() .collectList() .flatMapIterable(Function.identity()); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiService.java index 5f0d37690..19f5a50fb 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiService.java @@ -9,7 +9,7 @@ import java.util.List; public interface LibraryQueryApiService { - Mono> listLibraryQueries(); + Mono> listLibraryQueries(String name); Mono create(LibraryQuery libraryQuery); @@ -20,7 +20,7 @@ public interface LibraryQueryApiService { Mono publish(String libraryQueryId, LibraryQueryPublishRequest libraryQueryPublishRequest); @SuppressWarnings("ConstantConditions") - Mono> dropDownList(); + Mono> dropDownList(String name); Mono executeLibraryQueryFromJs(ServerWebExchange exchange, LibraryQueryRequestFromJs request); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiServiceImpl.java index 57505fca0..572f7ccdd 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryApiServiceImpl.java @@ -71,10 +71,11 @@ public class LibraryQueryApiServiceImpl implements LibraryQueryApiService { private int port; @Override - public Mono> listLibraryQueries() { + public Mono> listLibraryQueries(String name) { return orgDevChecker.checkCurrentOrgDev() .then(sessionUserService.getVisitorOrgMemberCache()) .flatMapMany(orgMember -> getByOrgIdWithDatasourcePermissions(orgMember.getOrgId())) + .filter(libraryQuery -> StringUtils.containsIgnoreCase(libraryQuery.getName(), name)) .collectList() .flatMap(libraryQueries -> ViewBuilder.multiBuild(libraryQueries, LibraryQuery::getCreatedBy, @@ -152,9 +153,10 @@ public Mono publish(String libraryQueryId, LibraryQu @Override @SuppressWarnings("ConstantConditions") - public Mono> dropDownList() { + public Mono> dropDownList(String name) { Mono> libraryQueryListMono = sessionUserService.getVisitorOrgMemberCache() .flatMapMany(orgMember -> getByOrgIdWithDatasourcePermissions(orgMember.getOrgId())) + .filter(libraryQuery -> StringUtils.containsIgnoreCase(libraryQuery.getName(), name)) .collectList() .cache(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java index dfbf317f5..dae78359c 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java @@ -16,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @@ -34,14 +35,14 @@ public class LibraryQueryController implements LibraryQueryEndpoints private GidService gidService; @Override - public Mono>> dropDownList() { - return libraryQueryApiService.dropDownList() + public Mono>> dropDownList(@RequestParam(required = false) String name) { + return libraryQueryApiService.dropDownList(name) .map(ResponseView::success); } @Override - public Mono>> list() { - return libraryQueryApiService.listLibraryQueries() + public Mono>> list(@RequestParam(required = false) String name) { + return libraryQueryApiService.listLibraryQueries(name) .map(ResponseView::success); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java index 9d493ddf0..97f6947d8 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java @@ -11,14 +11,7 @@ import org.lowcoder.api.query.view.LibraryQueryView; import org.lowcoder.api.query.view.UpsertLibraryQueryRequest; import org.lowcoder.domain.query.model.LibraryQuery; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import io.swagger.v3.oas.annotations.Operation; import reactor.core.publisher.Mono; @@ -36,7 +29,7 @@ public interface LibraryQueryEndpoints description = "Retrieve Library Queries in a dropdown format within Lowcoder, suitable for selection in user interfaces." ) @GetMapping("/dropDownList") - public Mono>> dropDownList(); + public Mono>> dropDownList(@RequestParam(required = false) String name); @Operation( tags = TAG_LIBRARY_QUERY_MANAGEMENT, @@ -45,7 +38,7 @@ public interface LibraryQueryEndpoints description = "Retrieve a list of Library Queries for a specific Organization within Lowcoder." ) @GetMapping("/listByOrg") - public Mono>> list(); + public Mono>> list(@RequestParam(required = false) String name); @Operation( tags = TAG_LIBRARY_QUERY_MANAGEMENT, diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java index 846b2483c..562d072f9 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java @@ -17,6 +17,7 @@ import org.lowcoder.domain.organization.service.OrganizationService; import org.lowcoder.domain.plugin.DatasourceMetaInfo; import org.lowcoder.domain.plugin.service.DatasourceMetaInfoService; +import org.lowcoder.domain.user.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.codec.multipart.Part; import org.springframework.web.bind.annotation.*; @@ -41,10 +42,12 @@ public class OrganizationController implements OrganizationEndpoints private OrgMemberService orgMemberService; @Autowired private OrganizationService organizationService; + @Autowired + private UserService userService; @Override - public Mono>> getOrganizationByUser(@PathVariable String userId) { - return orgMemberService.getAllActiveOrgs(userId) + public Mono>> getOrganizationByUser(@PathVariable String email) { + return userService.findByEmailDeep(email).flux().flatMap(user -> orgMemberService.getAllActiveOrgs(user.getId())) .flatMap(orgMember -> organizationService.getById(orgMember.getOrgId())) .map(OrgView::new) .collectList() diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java index a2c3dac6c..5ceaa5bd4 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java @@ -43,8 +43,8 @@ public interface OrganizationEndpoints summary = "Get a list of specified user's organization", description = "Get a list of specified user's organization" ) - @GetMapping("/byuser/{userId}") - public Mono>> getOrganizationByUser(@PathVariable String userId); + @GetMapping("/byuser/{email}") + public Mono>> getOrganizationByUser(@PathVariable String email); @Operation( tags = TAG_ORGANIZATION_MANAGEMENT, diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java index ba6b2d8c3..e533b42e5 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java @@ -7,6 +7,8 @@ import lombok.extern.slf4j.Slf4j; import org.bson.Document; import org.lowcoder.domain.application.model.Application; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; import org.lowcoder.domain.bundle.model.Bundle; import org.lowcoder.domain.datasource.model.Datasource; import org.lowcoder.domain.datasource.model.DatasourceStructureDO; @@ -27,9 +29,11 @@ import org.lowcoder.runner.migrations.job.AddSuperAdminUser; import org.lowcoder.runner.migrations.job.CompleteAuthType; import org.lowcoder.runner.migrations.job.MigrateAuthConfigJob; +import org.lowcoder.sdk.config.CommonConfig; import org.springframework.context.annotation.Profile; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.UncategorizedMongoDbException; +import org.springframework.data.mongodb.core.CollectionOptions; import org.springframework.data.mongodb.core.DocumentCallbackHandler; import org.springframework.data.mongodb.core.index.CompoundIndexDefinition; import org.springframework.data.mongodb.core.index.Index; @@ -39,6 +43,8 @@ import org.springframework.data.mongodb.core.query.Update; import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.Set; import static org.lowcoder.domain.util.QueryDslUtils.fieldName; @@ -273,7 +279,7 @@ public void processDocument(Document document) { @ChangeSet(order = "025", id = "add-gid-indexes-unique", author = "") public void addGidIndexesUnique(MongockTemplate mongoTemplate) { // collections to add gid - String[] collectionNames = {"group", "organization"}; + String[] collectionNames = {"group", "organization", "application", "bundle", "datasource", "libraryQuery", "folder"}; // Get the list of existing collections Set existingCollections = mongoTemplate.getCollectionNames(); @@ -295,6 +301,35 @@ public void addGidIndexesUnique(MongockTemplate mongoTemplate) { ensureIndexes(mongoTemplate, LibraryQuery.class, makeIndex("gid").unique()); } + @ChangeSet(order = "026", id = "add-time-series-snapshot-history", author = "") + public void addTimeSeriesSnapshotHistory(MongockTemplate mongoTemplate, CommonConfig commonConfig) { + // Create the time-series collection if it doesn't exist + if (!mongoTemplate.collectionExists(ApplicationHistorySnapshotTS.class)) { + mongoTemplate.createCollection(ApplicationHistorySnapshotTS.class, CollectionOptions.empty().timeSeries("createdAt")); + } + Instant thresholdDate = Instant.now().minus(commonConfig.getQuery().getAppSnapshotKeepDuration(), ChronoUnit.DAYS); + List snapshots = mongoTemplate.find(new Query().addCriteria(Criteria.where("createdAt").gte(thresholdDate)), ApplicationHistorySnapshot.class); + snapshots.forEach(snapshot -> { + ApplicationHistorySnapshotTS applicationHistorySnapshotTS = new ApplicationHistorySnapshotTS(); + applicationHistorySnapshotTS.setApplicationId(snapshot.getApplicationId()); + applicationHistorySnapshotTS.setDsl(snapshot.getDsl()); + applicationHistorySnapshotTS.setContext(snapshot.getContext()); + applicationHistorySnapshotTS.setCreatedAt(snapshot.getCreatedAt()); + applicationHistorySnapshotTS.setCreatedBy(snapshot.getCreatedBy()); + applicationHistorySnapshotTS.setModifiedBy(snapshot.getModifiedBy()); + applicationHistorySnapshotTS.setUpdatedAt(snapshot.getUpdatedAt()); + applicationHistorySnapshotTS.setId(snapshot.getId()); + mongoTemplate.insert(applicationHistorySnapshotTS); + mongoTemplate.remove(snapshot); + }); + + // Ensure indexes if needed + ensureIndexes(mongoTemplate, ApplicationHistorySnapshotTS.class, + makeIndex("applicationId"), + makeIndex("createdAt") + ); + } + private void addGidField(MongockTemplate mongoTemplate, String collectionName) { // Create a query to match all documents Query query = new Query(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/task/ArchiveSnapshotTask.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/task/ArchiveSnapshotTask.java new file mode 100644 index 000000000..2fa516379 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/task/ArchiveSnapshotTask.java @@ -0,0 +1,46 @@ +package org.lowcoder.runner.task; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; +import org.lowcoder.sdk.config.CommonConfig; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Slf4j +@RequiredArgsConstructor +@Component +public class ArchiveSnapshotTask { + + private final CommonConfig commonConfig; + private final MongoTemplate mongoTemplate; + + @Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.DAYS) + public void archive() { + Instant thresholdDate = Instant.now().minus(commonConfig.getQuery().getAppSnapshotKeepDuration(), ChronoUnit.DAYS); + List snapshots = mongoTemplate.find(new Query().addCriteria(Criteria.where("createdAt").lte(thresholdDate)), ApplicationHistorySnapshotTS.class); + snapshots.forEach(snapshot -> { + ApplicationHistorySnapshot applicationHistorySnapshot = new ApplicationHistorySnapshot(); + applicationHistorySnapshot.setApplicationId(snapshot.getApplicationId()); + applicationHistorySnapshot.setDsl(snapshot.getDsl()); + applicationHistorySnapshot.setContext(snapshot.getContext()); + applicationHistorySnapshot.setCreatedAt(snapshot.getCreatedAt()); + applicationHistorySnapshot.setCreatedBy(snapshot.getCreatedBy()); + applicationHistorySnapshot.setModifiedBy(snapshot.getModifiedBy()); + applicationHistorySnapshot.setUpdatedAt(snapshot.getUpdatedAt()); + applicationHistorySnapshot.setId(snapshot.getId()); + mongoTemplate.insert(applicationHistorySnapshot); + mongoTemplate.remove(snapshot); + }); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml b/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml index 5e17368b5..5f16fdace 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml +++ b/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml @@ -47,6 +47,8 @@ common: notifications-email-sender: ${LOWCODER_EMAIL_NOTIFICATIONS_SENDER:info@localhost} cookie: max-age-in-hours: ${LOWCODER_COOKIE_MAX_AGE:24} + query: + app-snapshot-keep-duration: ${LOWCODER_APP_SNAPSHOT_RETENTIONTIME:30} debug: true diff --git a/server/api-service/lowcoder-server/src/main/resources/application.yaml b/server/api-service/lowcoder-server/src/main/resources/application.yaml index 4184806b2..1ca536930 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application.yaml +++ b/server/api-service/lowcoder-server/src/main/resources/application.yaml @@ -88,6 +88,8 @@ common: notifications-email-sender: ${LOWCODER_EMAIL_NOTIFICATIONS_SENDER:info@localhost} cookie: max-age-in-hours: ${LOWCODER_COOKIE_MAX_AGE:24} + query: + app-snapshot-keep-duration: ${LOWCODER_APP_SNAPSHOT_RETENTIONTIME:30} material: mongodb-grid-fs: diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/datasource/DatasourceApiServiceIntegrationTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/datasource/DatasourceApiServiceIntegrationTest.java index f11b74c04..27f0e56ee 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/datasource/DatasourceApiServiceIntegrationTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/datasource/DatasourceApiServiceIntegrationTest.java @@ -54,7 +54,7 @@ public void testListOrgDatasource() { .flatMap(permissionItemView -> datasourceApiService.updatePermission(permissionItemView.getPermissionId(), VIEWER)) // create mysql05 .then(datasourceApiService.create(buildMysqlDatasource("mysql05"))) - .then(datasourceApiService.listOrgDataSources("org01").collectList()); + .then(datasourceApiService.listOrgDataSources("org01", null, null).collectList()); StepVerifier.create(datasourceListMono) .assertNext(datasourceViews -> { diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/query/LibraryQueryApiServiceIntegrationTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/query/LibraryQueryApiServiceIntegrationTest.java index 461811478..f4886ca5b 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/query/LibraryQueryApiServiceIntegrationTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/query/LibraryQueryApiServiceIntegrationTest.java @@ -47,7 +47,7 @@ public void beforeEach() { public void testListLibraryQueries() { Mono> listMono = datasourceApiService.create(DatasourceApiServiceIntegrationTest.buildMysqlDatasource("mysql06")) .flatMap(datasource -> libraryQueryApiService.create(buildLibraryQuery("query01", datasource.getId()))) - .then(libraryQueryApiService.listLibraryQueries()); + .then(libraryQueryApiService.listLibraryQueries(null)); StepVerifier.create(listMono) .assertNext(libraryQueryViews -> { diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java index 1413e01a2..c470c11d0 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/FolderApiServiceTest.java @@ -137,7 +137,7 @@ public void updateByGid() { public void move() { Mono> mono = folderApiService.move("app01", "folder02") - .then(folderApiService.getElements("folder02", null).collectList()); + .then(folderApiService.getElements("folder02", null, null).collectList()); StepVerifier.create(mono) .assertNext(list -> { diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java index d728dfc54..fb7109134 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java @@ -2,10 +2,9 @@ import com.google.common.collect.ImmutableMap; import lombok.extern.slf4j.Slf4j; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; import org.lowcoder.domain.application.service.ApplicationHistorySnapshotService; import org.lowcoder.sdk.models.HasIdAndAuditing; import org.springframework.beans.factory.annotation.Autowired; @@ -44,12 +43,12 @@ public void testServiceMethods() { .verifyComplete(); - StepVerifier.create(service.listAllHistorySnapshotBriefInfo(applicationId, PageRequest.of(0, 5))) + StepVerifier.create(service.listAllHistorySnapshotBriefInfo(applicationId, null, null, null, null, PageRequest.of(0, 5))) .assertNext(list -> { assertEquals(2, list.size()); - ApplicationHistorySnapshot first = list.get(0); - ApplicationHistorySnapshot second = list.get(1); + ApplicationHistorySnapshotTS first = list.get(0); + ApplicationHistorySnapshotTS second = list.get(1); assertTrue(first.getCreatedAt().isAfter(second.getCreatedAt())); assertNull(first.getDsl()); @@ -64,10 +63,10 @@ public void testServiceMethods() { }) .verifyComplete(); - StepVerifier.create(service.listAllHistorySnapshotBriefInfo(applicationId, PageRequest.of(1, 1))) + StepVerifier.create(service.listAllHistorySnapshotBriefInfo(applicationId, null, null, null, null, PageRequest.of(1, 1))) .assertNext(list -> { assertEquals(1, list.size()); - ApplicationHistorySnapshot one = list.get(0); + ApplicationHistorySnapshotTS one = list.get(0); assertNull(one.getDsl()); assertEquals(ImmutableMap.of("context", "context1"), one.getContext()); assertEquals(applicationId, one.getApplicationId()); @@ -75,7 +74,7 @@ public void testServiceMethods() { .verifyComplete(); - StepVerifier.create(service.listAllHistorySnapshotBriefInfo(applicationId, PageRequest.of(0, 5)) + StepVerifier.create(service.listAllHistorySnapshotBriefInfo(applicationId, null, null, null, null, PageRequest.of(0, 5)) .map(it -> it.get(0)) .map(HasIdAndAuditing::getId) .flatMap(id -> service.getHistorySnapshotDetail(id))) diff --git a/server/api-service/pom.xml b/server/api-service/pom.xml index 10c35fd82..200dfebf5 100644 --- a/server/api-service/pom.xml +++ b/server/api-service/pom.xml @@ -12,7 +12,7 @@ - 2.4.9 + 2.4.11 17 ${java.version} ${java.version} diff --git a/server/node-service/package.json b/server/node-service/package.json index 1152337c5..9827f6799 100644 --- a/server/node-service/package.json +++ b/server/node-service/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-node-server", - "version": "2.4.9", + "version": "2.4.11", "private": true, "engines": { "node": "^14.18.0 || >=16.0.0" diff --git a/server/node-service/src/plugins/firebase/dataSourceConfig.ts b/server/node-service/src/plugins/firebase/dataSourceConfig.ts index 86fe2cb5d..5cf3ce099 100644 --- a/server/node-service/src/plugins/firebase/dataSourceConfig.ts +++ b/server/node-service/src/plugins/firebase/dataSourceConfig.ts @@ -25,7 +25,7 @@ const dataSourceConfig = { label: "Private Key", type: "password", tooltip: - "The [document](https://firebase.google.com/docs/admin/setup) on how to obtain the private key.", + "The JSON [document](https://firebase.google.com/docs/admin/setup) on how to obtain the private key.", }, { label: "Spec Version", diff --git a/server/node-service/src/plugins/firebase/queryConfig.ts b/server/node-service/src/plugins/firebase/queryConfig.ts index 3e1dbae37..ddf4128ba 100644 --- a/server/node-service/src/plugins/firebase/queryConfig.ts +++ b/server/node-service/src/plugins/firebase/queryConfig.ts @@ -108,6 +108,12 @@ const queryConfig = { type: "numberInput", defaultValue: 10, }, + { + key: "startAt", + label: "Start at", + type: "textInput", + tooltip: "Start the query at a specific document.", + } ], }, { diff --git a/server/node-service/src/plugins/firebase/run.ts b/server/node-service/src/plugins/firebase/run.ts index 258405627..d837bc075 100644 --- a/server/node-service/src/plugins/firebase/run.ts +++ b/server/node-service/src/plugins/firebase/run.ts @@ -87,11 +87,22 @@ export async function runFirebasePlugin( const data = await withFirestoreCollection(async (ref) => { let query; if (actionData.orderBy) { + console.log("orderBy", actionData.orderBy); query = ref.orderBy( actionData.orderBy, (actionData.orderDirection || "asc") as OrderByDirection ); } + // Apply startAt if specified (for pagination) + if (actionData.startAt) { + if (Array.isArray(actionData.startAt)) { + // If startAt is an array, pass it as is + query = (query || ref).startAt(...actionData.startAt); + } else { + // If startAt is a single value, use it directly + query = (query || ref).startAt(actionData.startAt); + } + } if (actionData.limit > 0) { query = (query || ref).limit(actionData.limit); }