diff --git a/.github/workflows/publish-lowcoder-cli.yml b/.github/workflows/publish-lowcoder-cli.yml index 17eafa909..2aadbe488 100644 --- a/.github/workflows/publish-lowcoder-cli.yml +++ b/.github/workflows/publish-lowcoder-cli.yml @@ -9,6 +9,7 @@ on: jobs: publish-package: + if: ${{ github.repositoryUrl == 'git://github.com/lowcoder/lowcoder.git'}} runs-on: ubuntu-latest steps: - name: Checkout repository @@ -19,7 +20,9 @@ jobs: id: check with: diff-search: true + static-checking: localIsNew file-name: client/packages/lowcoder-cli/package.json + file-url: https://unpkg.com/lowcoder-cli@latest/package.json - name: Version update detected if: steps.check.outputs.changed == 'true' diff --git a/.github/workflows/publish-lowcoder-comps.yml b/.github/workflows/publish-lowcoder-comps.yml index 40eb567e0..27ffb60f9 100644 --- a/.github/workflows/publish-lowcoder-comps.yml +++ b/.github/workflows/publish-lowcoder-comps.yml @@ -9,6 +9,7 @@ on: jobs: publish-package: + if: ${{ github.repositoryUrl == 'git://github.com/lowcoder/lowcoder.git'}} runs-on: ubuntu-latest steps: - name: Checkout repository @@ -19,7 +20,9 @@ jobs: id: check with: diff-search: true + static-checking: localIsNew file-name: client/packages/lowcoder-comps/package.json + file-url: https://unpkg.com/lowcoder-comps@latest/package.json - name: Version update detected if: steps.check.outputs.changed == 'true' diff --git a/.github/workflows/publish-lowcoder-core.yml b/.github/workflows/publish-lowcoder-core.yml index 5d36c46c7..34222ad56 100644 --- a/.github/workflows/publish-lowcoder-core.yml +++ b/.github/workflows/publish-lowcoder-core.yml @@ -9,6 +9,7 @@ on: jobs: publish-package: + if: ${{ github.repositoryUrl == 'git://github.com/lowcoder/lowcoder.git'}} runs-on: ubuntu-latest steps: - name: Checkout repository @@ -19,7 +20,9 @@ jobs: id: check with: diff-search: true + static-checking: localIsNew file-name: client/packages/lowcoder-core/package.json + file-url: https://unpkg.com/lowcoder-core@latest/package.json - name: Version update detected if: steps.check.outputs.changed == 'true' diff --git a/.github/workflows/publish-lowcoder-sdk.yml b/.github/workflows/publish-lowcoder-sdk.yml index 40d93e71a..d9385b7d9 100644 --- a/.github/workflows/publish-lowcoder-sdk.yml +++ b/.github/workflows/publish-lowcoder-sdk.yml @@ -9,6 +9,7 @@ on: jobs: publish-package: + if: ${{ github.repositoryUrl == 'git://github.com/lowcoder/lowcoder.git'}} runs-on: ubuntu-latest steps: - name: Checkout repository @@ -19,7 +20,9 @@ jobs: id: check with: diff-search: true + static-checking: localIsNew file-name: client/packages/lowcoder-sdk/package.json + file-url: https://unpkg.com/lowcoder-sdk@latest/package.json - name: Version update detected if: steps.check.outputs.changed == 'true' diff --git a/client/packages/lowcoder/src/components/table/EditableCell.tsx b/client/packages/lowcoder/src/components/table/EditableCell.tsx index 799c21a26..a08a3607d 100644 --- a/client/packages/lowcoder/src/components/table/EditableCell.tsx +++ b/client/packages/lowcoder/src/components/table/EditableCell.tsx @@ -35,6 +35,7 @@ export interface CellProps { candidateTags?: string[]; candidateStatus?: { text: string; status: StatusType }[]; textOverflow?: boolean; + onTableEvent?: (eventName: any) => void; } export type CellViewReturn = (props: CellProps) => ReactNode; @@ -71,6 +72,7 @@ export function EditableCell(props: EditableCellProps) { baseValue, candidateTags, candidateStatus, + onTableEvent, } = props; const status = _.isNil(changeValue) ? "normal" : "toSave"; const editable = editViewFn ? props.editable : false; @@ -96,6 +98,9 @@ export function EditableCell(props: EditableCellProps) { false ) ); + if(!_.isEqual(tmpValue, value)) { + onTableEvent?.('columnEdited'); + } }, [dispatch, baseValue, tmpValue]); const editView = useMemo( () => editViewFn?.({ value, onChange, onChangeEnd }) ?? <>, diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/ColumnNumberComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/ColumnNumberComp.tsx index 20a390294..20622b3b2 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/ColumnNumberComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/ColumnNumberComp.tsx @@ -1,18 +1,40 @@ - import { default as Input } from "antd/es/input"; -import { NumberControl, StringControl } from "comps/controls/codeControl"; + import { default as InputNumber } from "antd/es/input-number"; +import { NumberControl, RangeControl, StringControl } from "comps/controls/codeControl"; import { BoolControl } from "comps/controls/boolControl"; import { trans } from "i18n"; import { ColumnTypeCompBuilder, ColumnTypeViewFn } from "../columnTypeCompBuilder"; import { ColumnValueTooltip } from "../simpleColumnTypeComps"; +import { withDefault } from "comps/generators"; +import styled from "styled-components"; + +const InputNumberWrapper = styled.div` + .ant-input-number { + width: 100%; + border-radius: 0; + background: transparent !important; + padding: 0 !important; + box-shadow: none; + + input { + padding: 0; + border-radius: 0; + } + } +`; const childrenMap = { text: NumberControl, + step: withDefault(NumberControl, 1), + precision: RangeControl.closed(0, 20, 0), float: BoolControl, prefix: StringControl, suffix: StringControl, }; let float = false; +let step = 1; +let precision = 0; + const getBaseValue: ColumnTypeViewFn = ( props ) => { @@ -24,26 +46,35 @@ export const ColumnNumberComp = (function () { childrenMap, (props, dispatch) => { float = props.float; - const value = !float ? Math.floor(props.changeValue ?? getBaseValue(props, dispatch)) : props.changeValue ?? getBaseValue(props, dispatch); - return props.prefix + value + props.suffix; + step = props.step; + precision = props.precision; + const value = props.changeValue ?? getBaseValue(props, dispatch); + let formattedValue: string | number = !float ? Math.floor(value) : value; + if(float) { + formattedValue = formattedValue.toPrecision(precision + 1); + } + return props.prefix + formattedValue + props.suffix; }, (nodeValue) => nodeValue.text.value, getBaseValue, ) .setEditViewFn((props) => { return ( - { - props.onChange(!float ? Math.floor(e.target.valueAsNumber) : e.target.valueAsNumber); - }} - onBlur={props.onChangeEnd} - onPressEnter={props.onChangeEnd} - /> + + { + value = value ?? 0; + props.onChange(!float ? Math.floor(value) : value); + }} + precision={float ? precision : 0} + onBlur={props.onChangeEnd} + onPressEnter={props.onChangeEnd} + /> + )}) .setPropertyViewFn((children) => { return ( @@ -52,17 +83,36 @@ export const ColumnNumberComp = (function () { label: trans("table.columnValue"), tooltip: ColumnValueTooltip, })} + {children.step.propertyView({ + label: trans("table.numberStep"), + tooltip: trans("table.numberStepTooltip"), + onFocus: (focused) => { + if(!focused) { + const value = children.step.getView(); + const isFloat = children.float.getView(); + const newValue = !isFloat ? Math.floor(value) : value; + children.step.dispatchChangeValueAction(String(newValue)); + } + } + })} + {float && ( + children.precision.propertyView({ + label: trans("table.precision"), + }) + )} {children.prefix.propertyView({ label: trans("table.prefix"), - // tooltip: ColumnValueTooltip, })} {children.suffix.propertyView({ label: trans("table.suffix"), - // tooltip: ColumnValueTooltip, })} {children.float.propertyView({ label: trans("table.float"), - // tooltip: ColumnValueTooltip, + onChange: (isFloat) => { + const value = children.step.getView(); + const newValue = !isFloat ? Math.floor(value) : value; + children.step.dispatchChangeValueAction(String(newValue)); + } })} ); diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx index 7329db720..06f7fcb38 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnLinksComp.tsx @@ -1,5 +1,3 @@ -import { EllipsisOutlined } from "@ant-design/icons"; -import { default as Dropdown} from "antd/es/dropdown"; import { default as Menu } from "antd/es/menu"; import { ColumnTypeCompBuilder } from "comps/comps/tableComp/column/columnTypeCompBuilder"; import { ActionSelectorControlInContext } from "comps/controls/actionSelector/actionSelectorControl"; @@ -12,18 +10,6 @@ import styled from "styled-components"; import { ColumnLink } from "comps/comps/tableComp/column/columnTypeComps/columnLinkComp"; import { LightActiveTextColor, PrimaryColor } from "constants/style"; -const LinksWrapper = styled.div` - white-space: nowrap; - - > a { - margin-right: 8px; - } - - > a:last-child { - margin-right: 0; - } -`; - const MenuLinkWrapper = styled.div` > a { color: ${PrimaryColor} !important; @@ -34,6 +20,22 @@ const MenuLinkWrapper = styled.div` } `; +const MenuWrapper = styled.div` + ul { + background: transparent !important; + border-bottom: 0; + + li { + padding: 0 10px 0 0 !important; + line-height: normal !important; + + &::after { + content: none !important; + } + } + } +`; + const OptionItem = new MultiCompBuilder( { label: StringControl, @@ -69,48 +71,28 @@ export const ColumnLinksComp = (function () { return new ColumnTypeCompBuilder( childrenMap, (props) => { - const menu = props.options.length > 3 && ( - - {props.options - .filter((o) => !o.hidden) - .slice(3) - .map((option, index) => ( - - - - - - ))} - - ); + const menuItems = props.options + .filter((o) => !o.hidden) + .map((option, index) => ( + { + key: index, + label: ( + + + + ) + } + )); return ( - - {props.options - .filter((o) => !o.hidden) - .slice(0, 3) - .map((option, i) => ( - - ))} - {menu && ( - menu} - > - e.preventDefault()} /> - - )} - - ); + + + + ) }, () => "" ) diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx index 348ce73fa..3e4137d9c 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx @@ -27,7 +27,6 @@ import { ColorControl } from "comps/controls/colorControl"; import { JSONValue } from "util/jsonTypes"; import styled from "styled-components"; import { TextOverflowControl } from "comps/controls/textOverflowControl"; -import { TableColumnLinkStyle, styleControl } from "@lowcoder-ee/index.sdk"; import { default as Divider } from "antd/es/divider"; export type Render = ReturnType["getOriginalComp"]>; export const RenderComp = withSelectedMultiContext(ColumnTypeComp); @@ -86,6 +85,7 @@ export type CellColorViewType = (param: { export const columnChildrenMap = { // column title title: StringControl, + showTitle: withDefault(BoolControl, true), // a custom column or a data column isCustom: valueComp(false), // If it is a data column, it must be the name of the column and cannot be duplicated as a react key @@ -100,6 +100,7 @@ export const columnChildrenMap = { fixed: dropdownControl(columnFixOptions, "close"), editable: BoolControl, background: withDefault(ColorControl, ""), + margin: withDefault(RadiusControl, ""), text: withDefault(ColorControl, ""), border: withDefault(ColorControl, ""), borderWidth: withDefault(RadiusControl, ""), @@ -149,6 +150,13 @@ export class ColumnComp extends ColumnInitComp { ) ); } + if(action.type === CompActionTypes.CHANGE_VALUE) { + const title = comp.children.title.unevaledValue; + const dataIndex = comp.children.dataIndex.getView(); + if(!Boolean(title)) { + comp.children.title.dispatchChangeValueAction(dataIndex); + } + } return comp; } @@ -180,9 +188,14 @@ export class ColumnComp extends ColumnInitComp { <> {this.children.title.propertyView({ label: trans("table.columnTitle"), + placeholder: this.children.dataIndex.getView(), })} {/* FIXME: cast type currently, return type of withContext should be corrected later */} {this.children.render.getPropertyView()} + {this.children.showTitle.propertyView({ + label: trans("table.showTitle"), + tooltip: trans("table.showTitleTooltip"), + })} {ColumnTypeCompMap[columnType].canBeEditable() && this.children.editable.propertyView({ label: trans("table.editable") })} {this.children.sortable.propertyView({ diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnListComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnListComp.tsx index f5429ba5c..d186aad0a 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnListComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnListComp.tsx @@ -101,7 +101,6 @@ export class ColumnListComp extends ColumnListTmpComp { dynamicColumn: boolean; data: Array; }) { - console.log("dataChangedAction", param); return customAction( { type: "dataChanged", diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx index 65555c29f..fdce5e83d 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx @@ -64,6 +64,10 @@ export class TableImplComp extends TableInitComp implements IContainer { override autoHeight(): boolean { return this.children.autoHeight.getView(); } + + getTableAutoHeight() { + return this.children.autoHeight.getView(); + } private getSlotContainer() { return this.children.expansion.children.slot.getSelectedComp().getComp().children.container; @@ -176,7 +180,6 @@ export class TableImplComp extends TableInitComp implements IContainer { override reduce(action: CompAction): this { let comp = super.reduce(action); - let dataChanged = false; if (action.type === CompActionTypes.UPDATE_NODES_V2) { const nextRowExample = tableDataRowExample(comp.children.data.getView()); @@ -316,10 +319,18 @@ export class TableImplComp extends TableInitComp implements IContainer { filter: this.children.toolbar.children.filter.node(), showFilter: this.children.toolbar.children.showFilter.node(), }; + let context = this; const filteredDataNode = withFunction(fromRecord(nodes), (input) => { const { data, searchValue, filter, showFilter } = input; const filteredData = filterData(data, searchValue.value, filter, showFilter.value); // console.info("filterNode. data: ", data, " filter: ", filter, " filteredData: ", filteredData); + // if data is changed on search then trigger event + if(Boolean(searchValue.value) && data.length !== filteredData.length) { + const onEvent = context.children.onEvent.getView(); + setTimeout(() => { + onEvent("dataSearch"); + }); + } return filteredData.map((row) => tranToTableRecord(row, row[OB_ROW_ORI_INDEX])); }); return lastValueIfEqual(this, "filteredDataNode", [filteredDataNode, nodes] as const, (a, b) => diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx index a523e7c58..ce17bc6ed 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx @@ -18,8 +18,11 @@ import { handleToSelectedRow, TableColumnLinkStyleType, TableColumnStyleType, + TableHeaderStyleType, TableRowStyleType, TableStyleType, + ThemeDetail, + TableToolbarStyleType, } from "comps/controls/styleControlConstants"; import { CompNameContext, EditorContext } from "comps/editorState"; import { BackgroundColorContext } from "comps/utils/backgroundColorContext"; @@ -27,7 +30,7 @@ import { PrimaryColor } from "constants/style"; import { trans } from "i18n"; import _ from "lodash"; import { darkenColor, isDarkColor } from "lowcoder-design"; -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { Children, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { Resizable } from "react-resizable"; import styled, { css } from "styled-components"; import { useUserViewMode } from "util/hooks"; @@ -39,15 +42,6 @@ import { messageInstance } from "lowcoder-design"; import { ReactRef, ResizeHandleAxis } from "layout/gridLayoutPropTypes"; import { CellColorViewType } from "./column/tableColumnComp"; -const TitleResizeHandle = styled.span` - position: absolute; - top: 0; - right: -5px; - width: 10px; - height: 100%; - cursor: col-resize; - z-index: 1; -`; function genLinerGradient(color: string) { return `linear-gradient(${color}, ${color})`; @@ -56,6 +50,8 @@ function genLinerGradient(color: string) { const getStyle = ( style: TableStyleType, rowStyle: TableRowStyleType, + headerStyle: TableHeaderStyleType, + toolbarStyle: TableToolbarStyleType ) => { const background = genLinerGradient(style.background); const selectedRowBackground = genLinerGradient(rowStyle.selectedRowBackground); @@ -64,7 +60,7 @@ const getStyle = ( return css` .ant-table-body { - background: white; + background: ${genLinerGradient(style.background)}; } .ant-table-tbody { > tr:nth-of-type(2n + 1) { @@ -126,21 +122,46 @@ const getStyle = ( `; }; +const TitleResizeHandle = styled.span` + position: absolute; + top: 0; + right: -5px; + width: 10px; + height: 100%; + cursor: col-resize; + z-index: 1; +`; + +const BackgroundWrapper = styled.div<{ + $style: TableStyleType; + $tableAutoHeight: boolean; +}>` + ${(props) => !props.$tableAutoHeight && `height: calc(100% - ${props.$style.margin} - ${props.$style.margin});`} + background: ${(props) => props.$style.background} !important; + border: ${(props) => `${props.$style.borderWidth} solid ${props.$style.border} !important`}; + border-radius: ${(props) => props.$style.radius} !important; + padding: unset !important; + margin: ${(props) => props.$style.margin} !important; + overflow: scroll !important; + ${(props) => props.$style} +`; + +// TODO: find a way to limit the calc function for max-height only to first Margin value const TableWrapper = styled.div<{ $style: TableStyleType; + $headerStyle: TableHeaderStyleType; + $toolbarStyle: TableToolbarStyleType; $rowStyle: TableRowStyleType; $toolbarPosition: "above" | "below" | "close"; $fixedHeader: boolean; $fixedToolbar: boolean; + $visibleResizables: boolean; + $showHRowGridBorder?: boolean; }>` - max-height: 100%; - overflow-y: auto; - background: white; - border: ${(props) => `1px solid ${props.$style.border}`}; - border-radius: ${(props) => props.$style.radius}; + overflow: unset !important; .ant-table-wrapper { - border-top: ${(props) => (props.$toolbarPosition === "above" ? "1px solid" : "unset")}; + border-top: unset; border-color: inherit; } @@ -187,10 +208,12 @@ const TableWrapper = styled.div<{ > .ant-table-thead { > tr > th { - background-color: ${(props) => props.$style.headerBackground}; - border-color: ${(props) => props.$style.border}; - color: ${(props) => props.$style.headerText}; - border-inline-end: ${(props) => `1px solid ${props.$style.border}`} !important; + background-color: ${(props) => props.$headerStyle.headerBackground}; + border-color: ${(props) => props.$headerStyle.border}; + border-width: ${(props) => props.$headerStyle.borderWidth}; + color: ${(props) => props.$headerStyle.headerText}; + font-size: ${(props) => props.$headerStyle.textSize}; + border-inline-end: ${(props) => `${props.$headerStyle.borderWidth} solid ${props.$headerStyle.border}`} !important; ${(props) => props.$fixedHeader && ` position: sticky; @@ -200,26 +223,32 @@ const TableWrapper = styled.div<{ ` } + > div { + margin: ${(props) => props.$headerStyle.margin}; + } + &:last-child { border-inline-end: none !important; } &.ant-table-column-has-sorters:hover { - background-color: ${(props) => darkenColor(props.$style.headerBackground, 0.05)}; + background-color: ${(props) => darkenColor(props.$headerStyle.headerBackground, 0.05)}; } > .ant-table-column-sorters > .ant-table-column-sorter { - color: ${(props) => props.$style.headerText === defaultTheme.textDark ? "#bfbfbf" : props.$style.headerText}; + color: ${(props) => props.$headerStyle.headerText === defaultTheme.textDark ? "#bfbfbf" : props.$headerStyle.headerText}; } &::before { - background-color: ${(props) => props.$style.border}; + background-color: ${(props) => props.$headerStyle.border}; + width: ${(props) => (props.$visibleResizables ? "1px" : "0px")} !important; } } } > thead > tr > th, > tbody > tr > td { - border-color: ${(props) => props.$style.border}; + border-color: ${(props) => props.$headerStyle.border}; + ${(props) => !props.$showHRowGridBorder && `border-bottom: 0px;`} } td { @@ -267,7 +296,7 @@ const TableWrapper = styled.div<{ } ${(props) => - props.$style && getStyle(props.$style, props.$rowStyle)} + props.$style && getStyle(props.$style, props.$rowStyle, props.$headerStyle, props.$toolbarStyle)} `; const TableTh = styled.th<{ width?: number }>` @@ -285,6 +314,7 @@ const TableTh = styled.th<{ width?: number }>` const TableTd = styled.td<{ $background: string; $style: TableColumnStyleType & {rowHeight?: string}; + $defaultThemeDetail: ThemeDetail; $linkStyle?: TableColumnLinkStyleType; $isEditing: boolean; $tableSize?: string; @@ -302,38 +332,42 @@ const TableTd = styled.td<{ border-color: ${(props) => props.$style.border} !important; border-width: ${(props) => props.$style.borderWidth} !important; border-radius: ${(props) => props.$style.radius}; + padding: 0 !important; > div { + margin: ${(props) => props.$style.margin}; color: ${(props) => props.$style.text}; - font-size: ${(props) => props.$style.textSize}; - line-height: 21px; - + ${(props) => props.$tableSize === 'small' && ` - padding: 8.5px 8px; - min-height: ${props.$style.rowHeight || '39px'}; + padding: 1px 8px; + font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '14px !important' : props.$style.textSize + ' !important' }; + min-height: ${props.$style.rowHeight || '14px'}; + line-height: 20px; ${!props.$autoHeight && ` overflow-y: auto; - max-height: ${props.$style.rowHeight || '39px'}; + max-height: ${props.$style.rowHeight || '28px'}; `}; `}; ${(props) => props.$tableSize === 'middle' && ` - padding: 12.5px 8px; - min-height: ${props.$style.rowHeight || '47px'}; + padding: 8px 8px; + font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '16px !important' : props.$style.textSize + ' !important' }; + min-height: ${props.$style.rowHeight || '24px'}; + line-height: 24px; ${!props.$autoHeight && ` overflow-y: auto; - max-height: ${props.$style.rowHeight || '47px'}; + max-height: ${props.$style.rowHeight || '48px'}; `}; `}; ${(props) => props.$tableSize === 'large' && ` - padding: 16.5px 16px; - min-height: ${props.$style.rowHeight || '55px'}; + padding: 16px 16px; + font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '18px !important' : props.$style.textSize + ' !important' }; + min-height: ${props.$style.rowHeight || '48px'}; ${!props.$autoHeight && ` overflow-y: auto; - max-height: ${props.$style.rowHeight || '55px'}; + max-height: ${props.$style.rowHeight || '96px'}; `}; `}; - > div > .ant-badge > .ant-badge-status-text, > div > div > .markdown-body { @@ -403,7 +437,7 @@ const ResizeableTitle = (props: any) => { draggableOpts={{ enableUserSelectHack: false }} handle={(axis: ResizeHandleAxis, ref: ReactRef) => ( { e.preventDefault(); e.stopPropagation(); @@ -419,6 +453,7 @@ const ResizeableTitle = (props: any) => { type CustomTableProps = Omit, "components" | "columns"> & { columns: CustomColumnType[]; viewModeResizable: boolean; + visibleResizables: boolean; rowColorFn: RowColorViewType; rowHeightFn: RowHeightViewType; columnsStyle: TableColumnStyleType; @@ -480,6 +515,7 @@ function TableCellView(props: { const style = { background: cellColor || rowColor || columnStyle.background || columnsStyle.background, + margin: columnStyle.margin || columnsStyle.margin, text: columnStyle.text || columnsStyle.text, border: columnStyle.border || columnsStyle.border, radius: columnStyle.radius || columnsStyle.radius, @@ -499,6 +535,7 @@ function TableCellView(props: { {...restProps} $background={background} $style={style} + $defaultThemeDetail={defaultTheme} $linkStyle={linkStyle} $isEditing={editing} $tableSize={tableSize} @@ -553,11 +590,6 @@ function ResizeableTable(props: CustomTableProps(props: CustomTableProps { - // if (width) { - // setResizeData({ - // index: index, - // width: width, - // }); - // } - // }, onResize: (width: React.SyntheticEvent) => { if (width) { setResizeData({ @@ -654,7 +678,12 @@ export function TableCompView(props: { const compChildren = comp.children; const style = compChildren.style.getView(); const rowStyle = compChildren.rowStyle.getView(); + const headerStyle = compChildren.headerStyle.getView(); + const toolbarStyle = compChildren.toolbarStyle.getView(); const rowAutoHeight = compChildren.rowAutoHeight.getView(); + const tableAutoHeight = comp.getTableAutoHeight(); + const visibleResizables = compChildren.visibleResizables.getView(); + const showHRowGridBorder = compChildren.showHRowGridBorder.getView(); const columnsStyle = compChildren.columnsStyle.getView(); const changeSet = useMemo(() => compChildren.columns.getChangeSet(), [compChildren.columns]); const hasChange = useMemo(() => !_.isEmpty(changeSet), [changeSet]); @@ -683,6 +712,7 @@ export function TableCompView(props: { dynamicColumn, dynamicColumnConfig, columnsAggrData, + onEvent, ), [ columnViews, @@ -735,7 +765,7 @@ export function TableCompView(props: { const toolbarView = ( onDownload(`${compName}-data`)} + onDownload={() => { + handleChangeEvent("download"); + onDownload(`${compName}-data`) + }} hasChange={hasChange} onSaveChanges={() => handleChangeEvent("saveChanges")} onCancelChanges={() => handleChangeEvent("cancelChanges")} @@ -761,56 +794,68 @@ export function TableCompView(props: { } return ( - -
- + + + {toolbar.position === "above" && toolbarView} - - expandable={{ - ...expansion.expandableConfig, - childrenColumnName: supportChildren - ? COLUMN_CHILDREN_KEY - : "OB_CHILDREN_KEY_PLACEHOLDER", - fixed: "left", - onExpand: (expanded) => { - if(expanded) handleChangeEvent('rowExpand') + + + expandable={{ + ...expansion.expandableConfig, + childrenColumnName: supportChildren + ? COLUMN_CHILDREN_KEY + : "OB_CHILDREN_KEY_PLACEHOLDER", + fixed: "left", + onExpand: (expanded) => { + if(expanded) { + handleChangeEvent('rowExpand') + } else { + handleChangeEvent('rowShrink') + } + } + }} + rowColorFn={compChildren.rowColor.getView() as any} + rowHeightFn={compChildren.rowHeight.getView() as any} + {...compChildren.selection.getView()(onEvent)} + bordered={compChildren.showRowGridBorder.getView()} + onChange={(pagination, filters, sorter, extra) => { + onTableChange(pagination, filters, sorter, extra, comp.dispatch, onEvent); + }} + showHeader={!compChildren.hideHeader.getView()} + columns={antdColumns} + columnsStyle={columnsStyle} + viewModeResizable={compChildren.viewModeResizable.getView()} + visibleResizables={compChildren.visibleResizables.getView()} + dataSource={pageDataInfo.data} + size={compChildren.size.getView()} + rowAutoHeight={rowAutoHeight} + tableLayout="fixed" + loading={ + loading || + // fixme isLoading type + (compChildren.showDataLoadSpinner.getView() && + (compChildren.data as any).isLoading()) || + compChildren.loading.getView() } - }} - rowColorFn={compChildren.rowColor.getView() as any} - rowHeightFn={compChildren.rowHeight.getView() as any} - {...compChildren.selection.getView()(onEvent)} - bordered={!compChildren.hideBordered.getView()} - onChange={(pagination, filters, sorter, extra) => { - onTableChange(pagination, filters, sorter, extra, comp.dispatch, onEvent); - }} - showHeader={!compChildren.hideHeader.getView()} - columns={antdColumns} - columnsStyle={columnsStyle} - viewModeResizable={compChildren.viewModeResizable.getView()} - dataSource={pageDataInfo.data} - size={compChildren.size.getView()} - rowAutoHeight={rowAutoHeight} - tableLayout="fixed" - loading={ - loading || - // fixme isLoading type - (compChildren.showDataLoadSpinner.getView() && - (compChildren.data as any).isLoading()) || - compChildren.loading.getView() - } - /> + /> + + + {expansion.expandModalView} + + {toolbar.position === "below" && toolbarView} - - {expansion.expandModalView} - - -
+ +
); } diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableDynamicColumn.test.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableDynamicColumn.test.tsx index c5e1b65a0..022e0558b 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableDynamicColumn.test.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableDynamicColumn.test.tsx @@ -36,6 +36,7 @@ const expectColumn = ( // with dynamic config const dynamicColumnConfig = comp.children.dynamicColumnConfig.getView(); if (dynamicColumnConfig?.length > 0) { + const onEvent = (eventName: any) => {}; const antdColumns = columnsToAntdFormat( columnViews, comp.children.sort.getView(), @@ -43,7 +44,8 @@ const expectColumn = ( comp.children.size.getView(), comp.children.dynamicColumn.getView(), dynamicColumnConfig, - comp.columnAggrData + comp.columnAggrData, + onEvent, ); expect(columnViews.length).toBeGreaterThanOrEqual(antdColumns.length); antdColumns.forEach((column) => { diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx index 065806c1c..58f8145c0 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx @@ -469,13 +469,14 @@ export function compTablePropertyView {comp.children.hideHeader.propertyView({ label: trans("table.hideHeader"), })} - {comp.children.hideBordered.propertyView({ - label: trans("table.hideBordered"), - })} {comp.children.viewModeResizable.propertyView({ label: trans("table.viewModeResizable"), tooltip: trans("table.viewModeResizableTooltip"), })} + {comp.children.visibleResizables.propertyView({ + label: trans("table.visibleResizables"), + tooltip: trans("table.visibleResizablesTooltip"), + })}
{comp.children.pagination.getPropertyView()} @@ -507,9 +508,21 @@ export function compTablePropertyView {["layout", "both"].includes(editorModeStatus) && ( <>
- {comp.children.style.getPropertyView()} + {comp.children.style.getPropertyView()} +
+
+ {comp.children.headerStyle.getPropertyView()} +
+
+ {comp.children.toolbarStyle.getPropertyView()}
+ {comp.children.showRowGridBorder.propertyView({ + label: trans("table.showVerticalRowGridBorder"), + })} + {comp.children.showHRowGridBorder.propertyView({ + label: trans("table.showHorizontalRowGridBorder"), + })} {comp.children.rowStyle.getPropertyView()} {comp.children.rowAutoHeight.getPropertyView()} {comp.children.rowHeight.getPropertyView()} diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx index f7e8829e0..d64ffc8b1 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableToolbarComp.tsx @@ -7,7 +7,7 @@ import { TableOnEventView } from "comps/comps/tableComp/tableTypes"; import { BoolControl } from "comps/controls/boolControl"; import { StringControl } from "comps/controls/codeControl"; import { dropdownControl } from "comps/controls/dropdownControl"; -import { defaultTheme, TableStyleType } from "comps/controls/styleControlConstants"; +import { defaultTheme, TableToolbarStyleType } from "comps/controls/styleControlConstants"; import { stateComp } from "comps/generators"; import { genRandomKey } from "comps/utils/idGenerator"; import { ThemeContext } from "comps/utils/themeContext"; @@ -46,19 +46,20 @@ const SaveChangeButtons = styled.div` `; const getStyle = ( - style: TableStyleType, + style: TableToolbarStyleType, filtered: boolean, theme: ThemeDetail, position: ToolbarRowType["position"], fixedToolbar: boolean, ) => { return css` - background-color: ${style.toolbarBackground}; + background-color: ${style.background}; // Implement horizontal scrollbar and vertical page number selection is not blocked padding: 13px 12px; position: sticky; postion: -webkit-sticky; - left: 0; + left: 0px !important; + margin: ${style.margin} !important; ${fixedToolbar && `z-index: 99;`}; ${fixedToolbar && position === 'below' && `bottom: 0;`}; @@ -125,7 +126,7 @@ const getStyle = ( } .ant-pagination-item-active { - border-color: ${theme?.primary}; + border-color: ${style.border || theme?.primary}; a { color: ${theme?.textDark}; @@ -145,13 +146,13 @@ const getStyle = ( .ant-select-selector, .ant-pagination-options-quick-jumper input:hover, .ant-pagination-options-quick-jumper input:focus { - border-color: ${theme?.primary}; + border-color: ${style.border || theme?.primary}; } `; }; const ToolbarWrapper = styled.div<{ - $style: TableStyleType; + $style: TableToolbarStyleType; $filtered: boolean; $theme: ThemeDetail; $position: ToolbarRowType["position"]; @@ -609,7 +610,7 @@ function ColumnSetting(props: { allChecked = false; } return ( - + { @@ -677,6 +678,7 @@ function ToolbarPopover(props: { ; export function TableToolbar(props: { toolbar: ToolbarRowType; - $style: TableStyleType; + $style: TableToolbarStyleType; pagination: PaginationProps; columns: Array; onRefresh: () => void; diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx index 58d0b4974..90f5f0a6c 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx @@ -13,7 +13,7 @@ import { import { dropdownControl } from "comps/controls/dropdownControl"; import { eventHandlerControl } from "comps/controls/eventHandlerControl"; import { styleControl } from "comps/controls/styleControl"; -import { TableColumnStyle, TableRowStyle, TableStyle } from "comps/controls/styleControlConstants"; +import { TableColumnStyle, TableRowStyle, TableStyle, TableToolbarStyle, TableHeaderStyle } from "comps/controls/styleControlConstants"; import { MultiCompBuilder, stateComp, @@ -78,6 +78,26 @@ export const TableEventOptions = [ value: "rowExpand", description: trans("table.rowExpand"), }, + { + label: trans("table.rowShrink"), + value: "rowShrink", + description: trans("table.rowShrink"), + }, + { + label: trans("table.columnEdited"), + value: "columnEdited", + description: trans("table.columnEdited"), + }, + { + label: trans("table.search"), + value: "dataSearch", + description: trans("table.search"), + }, + { + label: trans("table.download"), + value: "download", + description: trans("table.download"), + }, { label: trans("table.filterChange"), value: "filterChange", @@ -170,7 +190,10 @@ export type RowHeightViewType = (param: { }) => string; const tableChildrenMap = { - hideBordered: BoolControl, + // hideBordered: BoolControl, + showHeaderGridBorder: BoolControl, + showRowGridBorder: BoolControl, + showHRowGridBorder: BoolControl, hideHeader: BoolControl, fixedHeader: BoolControl, autoHeight: withDefault(AutoHeightControl, "auto"), @@ -184,15 +207,19 @@ const tableChildrenMap = { toolbar: TableToolbarComp, style: styleControl(TableStyle), rowStyle: styleControl(TableRowStyle), + toolbarStyle: styleControl(TableToolbarStyle), + headerStyle: styleControl(TableHeaderStyle), searchText: StringControl, columnsStyle: withDefault(styleControl(TableColumnStyle), {borderWidth: '1px', radius: '0px'}), viewModeResizable: BoolControl, + visibleResizables: BoolControl, // sample data for regenerating columns dataRowExample: stateComp(null), onEvent: TableEventControl, loading: BoolCodeControl, rowColor: RowColorComp, rowAutoHeight: withDefault(AutoHeightControl, "auto"), + tableAutoHeight: withDefault(AutoHeightControl, "auto"), rowHeight: RowHeightComp, dynamicColumn: BoolPureControl, // todo: support object config diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx index 5e30837e3..d1573274b 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx @@ -269,6 +269,7 @@ export function columnsToAntdFormat( dynamicColumn: boolean, dynamicColumnConfig: Array, columnsAggrData: ColumnsAggrData, + onTableEvent: (eventName: any) => void, ): Array> { const sortMap: Map = new Map( sort.map((s) => [s.column, s.desc ? "descend" : "ascend"]) @@ -311,7 +312,7 @@ export function columnsToAntdFormat( }[]; const title = renderTitle({ title: column.title, editable: column.editable }); return { - title: title, + title: column.showTitle ? title : '', titleText: column.title, dataIndex: column.dataIndex, align: column.align, @@ -319,6 +320,7 @@ export function columnsToAntdFormat( fixed: column.fixed === "close" ? false : column.fixed, style: { background: column.background, + margin: column.margin, text: column.text, border: column.border, radius: column.radius, @@ -346,9 +348,11 @@ export function columnsToAntdFormat( .getView() .view({ editable: column.editable, - size, candidateTags: tags, + size, + candidateTags: tags, candidateStatus: status, textOverflow: column.textOverflow, + onTableEvent, }); }, ...(column.sortable diff --git a/client/packages/lowcoder/src/comps/controls/boolControl.tsx b/client/packages/lowcoder/src/comps/controls/boolControl.tsx index 7ea77e88e..b044138fa 100644 --- a/client/packages/lowcoder/src/comps/controls/boolControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/boolControl.tsx @@ -106,7 +106,7 @@ class BoolControl extends AbstractComp({ useCodeEditor: !this.useCodeEditor }, true); } - propertyView(params: ControlParams) { + propertyView(params: ControlParams & {onChange?: (changed: boolean) => void}) { const changeModeIcon = ( this.dispatchChangeValueAction(x)} + onChange={(x) => { + this.dispatchChangeValueAction(x); + params?.onChange?.(x); + }} > )} diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 14308f32b..cd39479fe 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -157,7 +157,10 @@ export function handleToSelectedRow(color: string, primary: string = defaultThem // return table header background color export function handleToHeadBg(color: string) { if (toHex(color) === SURFACE_COLOR) { - return "#FAFAFA"; + return darkenColor(color, 0.06); + } + if (toHex(color) === "#000000") { + return SECOND_SURFACE_COLOR; } if (isDarkColor(color)) { return darkenColor(color, 0.06); @@ -683,13 +686,42 @@ const LinkTextStyle = [ ] as const; export const TableStyle = [ + MARGIN, ...BG_STATIC_BORDER_RADIUS, + { + name: "borderWidth", + label: trans("style.borderWidth"), + borderWidth: "borderWidth", + }, +] as const; + +export const TableToolbarStyle = [ + MARGIN, + getBackground(), + getStaticBorder(), + { + name: "toolbarText", + label: trans("style.toolbarText"), + depName: "toolbarBackground", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: toSelf, + }, +] as const; + +export const TableHeaderStyle = [ + MARGIN, { name: "headerBackground", label: trans("style.tableHeaderBackground"), - depName: "background", + depName: "headerBackground", transformer: handleToHeadBg, }, + getStaticBorder(), + { + name: "borderWidth", + label: trans("style.borderWidth"), + borderWidth: "borderWidth", + }, { name: "headerText", label: trans("style.tableHeaderText"), @@ -697,20 +729,7 @@ export const TableStyle = [ depType: DEP_TYPE.CONTRAST_TEXT, transformer: contrastText, }, - { - name: "toolbarBackground", - label: trans("style.toolbarBackground"), - depName: "background", - depType: DEP_TYPE.SELF, - transformer: toSelf, - }, - { - name: "toolbarText", - label: trans("style.toolbarText"), - depName: "toolbarBackground", - depType: DEP_TYPE.CONTRAST_TEXT, - transformer: contrastText, - }, + TEXT_SIZE, ] as const; export const TableRowStyle = [ @@ -740,6 +759,7 @@ export const TableRowStyle = [ export const TableColumnStyle = [ getStaticBackground("#00000000"), getStaticBorder(), + MARGIN, BORDER_WIDTH, RADIUS, TEXT, @@ -1073,6 +1093,8 @@ export type CheckboxStyleType = StyleConfigType; export type RadioStyleType = StyleConfigType; export type SegmentStyleType = StyleConfigType; export type TableStyleType = StyleConfigType; +export type TableHeaderStyleType = StyleConfigType; +export type TableToolbarStyleType = StyleConfigType; export type TableRowStyleType = StyleConfigType; export type TableColumnStyleType = StyleConfigType; export type TableColumnLinkStyleType = StyleConfigType; @@ -1152,3 +1174,5 @@ export function marginCalculator(margin: string) { return parseInt(marginArr[0]?.replace(/[^\d.]/g, "") || "0") + parseInt(marginArr[2]?.replace(/[^\d.]/g, "") || "0") } } +export type { ThemeDetail }; + diff --git a/client/packages/lowcoder/src/i18n/locales/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index e049fb2f0..298e80ef4 100644 --- a/client/packages/lowcoder/src/i18n/locales/de.ts +++ b/client/packages/lowcoder/src/i18n/locales/de.ts @@ -1124,7 +1124,9 @@ export const de = { "columnValueTooltip": "\\'{{currentCell}}\\': Aktuelle Zelldaten\n \\'{{currentRow}}\\': Aktuelle Zeilendaten\n \\'{{currentIndex}}\\': Aktueller Datenindex (beginnend bei 0)\n Beispiel: \\'{{currentCell * 5}}\\' Show 5 Times the Original Value Data.", "imageSrc": "Bildquelle", "imageSize": "Bildgröße", - "columnTitle": "Titel", + "columnTitle": "Titel anzeigen", + "showTitle": "Show Title", + "showTitleTooltip": "Spaltentitel im Tabellenkopf ein-/ausblenden", "sortable": "Sortierbar", "align": "Ausrichtung", "fixedColumn": "Feste Säule", @@ -1133,6 +1135,9 @@ export const de = { "auto": "Auto", "fixed": "Festgelegt", "columnType": "Säule Typ", + "numberStep": "Schritt", + "numberStepTooltip": "Die Zahl, auf die der aktuelle Wert erhöht oder verringert wird. Es kann eine ganze Zahl oder eine Dezimalzahl sein", + "precision": "Präzision", "float": "Schwimmer", "prefix": "Präfix", "suffix": "Nachsilbe", @@ -1186,7 +1191,11 @@ export const de = { "cancelChanges": "Änderungen abbrechen", "rowSelectChange": "Zeile auswählen Ändern", "rowClick": "Reihe Klicken", - "rowExpand": "Reihe Erweitern", + "rowExpand": "Reihe verkleinern", + "rowShrink": "Zeilenverkleinerung", + "search": "Suchen", + "download": "Herunterladen", + "columnEdited": "Spalte bearbeitet", "filterChange": "Filterwechsel", "sortChange": "Sortieren Ändern", "pageChange": "Seitenwechsel", diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index a75348a04..c4f32115a 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1191,6 +1191,8 @@ export const en = { "columnNum": "Columns", "viewModeResizable": "Column Width Adjusted by User", "viewModeResizableTooltip": "Whether Users Can Adjust Column Width.", + "visibleResizables": "Show Resize Handles", + "visibleResizablesTooltip": "Display visible Resize Handles in the Table Header.", "showFilter": "Show Filter Button", "showRefresh": "Show Refresh Button", "showDownload": "Show Download Button", @@ -1229,6 +1231,8 @@ export const en = { "imageSrc": "Image Source", "imageSize": "Image Size", "columnTitle": "Title", + "showTitle": "Show Title", + "showTitleTooltip": "Show/Hide column title in table header", "sortable": "Sortable", "align": "Alignment", "fixedColumn": "Fixed Column", @@ -1237,6 +1241,9 @@ export const en = { "auto": "Auto", "fixed": "Fixed", "columnType": "Column Type", + "numberStep": "Step", + "numberStepTooltip": "The number to which the current value is increased or decreased. It can be an integer or decimal", + "precision": "Precision", "float": "Float", "prefix": "Prefix", "suffix": "Suffix", @@ -1268,7 +1275,9 @@ export const en = { "fixedHeaderTooltip": "Header Will Be Fixed for Vertically Scrollable Table", "fixedToolbar": "Fixed Toolbar", "fixedToolbarTooltip": "Toolbar Will Be Fixed for Vertically Scrollable Table Based on Position", - "hideBordered": "Hide Column Border", + "hideBordered": "Show Resize Handles", + "showVerticalRowGridBorder": "Show vertical Row Grid Border", + "showHorizontalRowGridBorder": "Show horizontal Row Grid Border", "deleteColumn": "Delete Column", "confirmDeleteColumn": "Confirm Delete Column: ", "small": "S", @@ -1292,6 +1301,10 @@ export const en = { "rowSelectChange": "Row Select Change", "rowClick": "Row Click", "rowExpand": "Row Expand", + "rowShrink": "Row Shrink", + "search": "Search", + "download": "Download", + "columnEdited": "Column Edited", "filterChange": "Filter Change", "sortChange": "Sort Change", "pageChange": "Page Change", diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 778a08885..9d80b50d3 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -1157,6 +1157,8 @@ table: { columnNum: "列数", viewModeResizable: "用户调整列宽", viewModeResizableTooltip: "是否允许调整列宽.", + "visibleResizables": "Show Resize Handles", + "visibleResizablesTooltip": "Display visible Resize Handles in the Table Header.", showFilter: "显示筛选按钮", showRefresh: "显示刷新按钮", showDownload: "显示下载按钮", @@ -1200,6 +1202,8 @@ table: { imageSrc: "图片链接", imageSize: "图片尺寸", columnTitle: "标题", + showTitle: "显示标题", + showTitleTooltip: "显示/隐藏表标题中的列标题", sortable: "可排序", align: "对齐方式", fixedColumn: "固定列", @@ -1208,6 +1212,9 @@ table: { auto: "自动", fixed: "固定", columnType: "列类型", + numberStep: "步", + numberStepTooltip: "当前值增加或减少的数量。它可以是整数或小数", + precision: "精度", float: "分数", prefix: "字首", suffix: "后缀", @@ -1216,6 +1223,7 @@ table: { link: "链接", links: "多链接", tag: "标签", + select: "下拉单选", date: "日期", dateTime: "日期时间", badgeStatus: "状态", @@ -1237,6 +1245,8 @@ table: { fixedHeader: "固定表头", fixedHeaderTooltip: "垂直滚动表格的标题将被固定", hideBordered: "隐藏列边框", + "showHeaderGridBorder": "显示标题网格边框", + "showRowGridBorder": "显示行网格边框", deleteColumn: "删除列", confirmDeleteColumn: "确认删除列:", small: "小", @@ -1263,6 +1273,10 @@ table: { rowSelectChange: "行选中变化", rowClick: "行点击", rowExpand: "行展开", + rowShrink: "行收缩", + search: "搜索", + download: "下载", + columnEdited: "栏目已编辑", filterChange: "筛选变化", sortChange: "排序变化", pageChange: "分页变化", diff --git a/deploy/docker/docker-compose-multi.yaml b/deploy/docker/docker-compose-multi.yaml index 0c05848e0..c387ac58d 100644 --- a/deploy/docker/docker-compose-multi.yaml +++ b/deploy/docker/docker-compose-multi.yaml @@ -57,6 +57,7 @@ services: # - on linux/mac, generate one eg. with: head /dev/urandom | head -c 30 | shasum -a 256 # LOWCODER_API_KEY_SECRET: "5a41b090758b39b226603177ef48d73ae9839dd458ccb7e66f7e7cc028d5a50b" + COMMON_WORKSPACE_MODE: SAAS restart: unless-stopped depends_on: - mongodb diff --git a/deploy/docker/docker-compose.yaml b/deploy/docker/docker-compose.yaml index 4a39ee6af..4917a2e9e 100644 --- a/deploy/docker/docker-compose.yaml +++ b/deploy/docker/docker-compose.yaml @@ -50,6 +50,7 @@ services: # frontend parameters LOWCODER_MAX_REQUEST_SIZE: 20m LOWCODER_MAX_QUERY_TIMEOUT: 120 + COMMON_WORKSPACE_MODE: SAAS volumes: - ./lowcoder-stacks:/lowcoder-stacks - ./lowcoder-stacks/assets:/lowcoder/assets diff --git a/deploy/helm/Chart.yaml b/deploy/helm/Chart.yaml index b09168049..0e6538eaf 100644 --- a/deploy/helm/Chart.yaml +++ b/deploy/helm/Chart.yaml @@ -4,7 +4,7 @@ description: A Helm chart for Kubernetes for installing lowcoder type: application # Chart version (change every time you make changes to the chart) -version: 0.1.0 +version: 1.0.0 # Lowcoder version appVersion: "latest" diff --git a/deploy/helm/README.md b/deploy/helm/README.md index a9854f7a4..b7dd8555b 100644 --- a/deploy/helm/README.md +++ b/deploy/helm/README.md @@ -38,6 +38,28 @@ $ helm delete -n lowcoder my-lowcoder ## Parameters +## Global + +| Name | Description | Value | +| --------------------------------------- | --------------------------------------------------------------------------------- | -------------- | +| `global.config.workspaceMode` | Sets the workspace mode. Possible types are: SAAS, ENTERPRISE | `SAAS` | +| `global.config.userId` | User ID of user running Lowcoder server application in container | `9001` | +| `global.config.groupId` | Group ID of user running Lowcoder server application in container | `9001` | +| `global.config.corsAllowedDomains` | CORS allowed domains | `*` | +| `global.config.enableUserSignUp` | Enable users signing up to lowcoder via login page | `true` | +| `global.config.encryption.password` | Encryption password - CHANGE IT! | `lowcoder.org` | +| `global.config.encryption.salt` | Encryption salt - CHANGE IT! | `lowcoder.org` | +| `global.config.apiKeySecret` | API-KEY secret, should be a string of at least 32 random characters - CHANGE IT | `5a41b090758b39b226603177ef48d73ae9839dd458ccb7e66f7e7cc028d5a50b` | +| `global.config.maxQueryTimeout` | Maximum query timeout in seconds | `120` | +| `global.config.maxRequestSize` | Maximum request size | `20m` | +| `global.config.nodeServiceUrl` | URL to node-service server if using external one (disabled by default) | | +| `global.config.apiServiceUrl` | URL to api-service server if using external one (disabled by default) | | +| `global.defaults.maxOrgsPerUser` | Maximum allowed organizations per user | `100` | +| `global.defaults.maxMembersPerOrg` | Maximum allowed members per organization | `1000` | +| `global.defaults.maxGroupsPerOrg` | Maximum groups allowed per organization | `100` | +| `global.defaults.maxAppsPerOrg` | Maximum allowed applications per organization | `1000` | +| `global.defaults.maxDevelopers` | Maximum allowed developer accounts | `100` | + ### Redis | Name | Description | Value | @@ -56,32 +78,3 @@ All available parameters can be found in [Bitnami Redis Chart](https://github.co All available parameters can be found in [Bitnami MongoDB Chart](https://github.com/bitnami/charts/tree/main/bitnami/mongodb/#parameters) -### Lowcoder server api-service - -| Name | Description | Value | -| --------------------------------------- | --------------------------------------------------------------------------- | ---------------- | -| `apiService.config.userId` | User ID of user running Lowcoder server application in container | `9001` | -| `apiService.config.groupId` | Group ID of user running Lowcoder server application in container | `9001` | -| `apiService.config.corsAllowedDomains` | CORS allowed domains | `*` | -| `apiService.config.encryption.password` | Encryption password | `lowcoder.org` | -| `apiService.config.encryption.salt` | Encryption salt | `lowcoder.org` | -| `apiService.config.enableUserSignUp` | Enable users signing up to lowcoder via login page | `true` | -| `apiService.config.nodeServiceUrl` | URL to node-service server if using external Lowcoder server | | - -### Lowcoder server node-service - -| Name | Description | Value | -| --------------------------------------- | --------------------------------------------------------------------------- | ---------------- | -| `nodeService.config.userId` | User ID of user running Lowcoder service application in container | `9001` | -| `nodeService.config.groupId` | Group ID of user running Lowcoder service application in container | `9001` | -| `nodeService.config.apiServiceUrl` | URL to api-service server if using external Lowcoder server | | - -### Lowcoder frontend (client) - -| Name | Description | Value | -| --------------------------------------- | --------------------------------------------------------------------------- | ---------------- | -| `frontend.config.userId` | User ID of nginx user running Lowcoder client application in container | `9001` | -| `frontend.config.groupId` | Group ID of nginx user running Lowcoder client application in container | `9001` | -| `frontend.config.apiServiceUrl` | URL to api-service server if using external Lowcoder server | `""` | -| `frontend.config.nodeServiceUrl` | URL to node-service server if using external Lowcoder server | | - diff --git a/deploy/helm/templates/api-service/configMap.yaml b/deploy/helm/templates/api-service/configMap.yaml index 6ea3f796b..09049bc9a 100644 --- a/deploy/helm/templates/api-service/configMap.yaml +++ b/deploy/helm/templates/api-service/configMap.yaml @@ -17,13 +17,19 @@ data: {{- else }} REDIS_URL: {{ .Values.redis.externalUrl | quote }} {{- end }} - {{- if .Values.apiService.nodeServiceUrl }} - LOWCODER_NODE_SERVICE_URL: {{ .Values.apiService.nodeServiceUrl | quote }} + {{- if .Values.global.config.nodeServiceUrl }} + LOWCODER_NODE_SERVICE_URL: {{ .Values.global.config.nodeServiceUrl | quote }} {{- else }} LOWCODER_NODE_SERVICE_URL: "http://{{ $name }}-node-service:{{ .Values.nodeService.service.port }}" {{- end }} - PUID: {{ .Values.apiService.config.userId | default "9001" | quote }} - PGID: {{ .Values.apiService.config.groupId | default "9001" | quote }} - CORS_ALLOWED_DOMAINS: {{ .Values.apiService.config.corsAllowedDomains | default "*" | quote }} - ENABLE_USER_SIGN_UP: {{ .Values.apiService.config.enableUserSignUp | default "true" | quote }} - + PUID: {{ .Values.global.config.userId | default "9001" | quote }} + PGID: {{ .Values.global.config.groupId | default "9001" | quote }} + CORS_ALLOWED_DOMAINS: {{ .Values.global.config.corsAllowedDomains | default "*" | quote }} + ENABLE_USER_SIGN_UP: {{ .Values.global.config.enableUserSignUp | default "true" | quote }} + LOWCODER_MAX_QUERY_TIMEOUT: {{ .Values.global.config.maxQueryTimeout | default "120" | quote }} + DEFAULT_ORGS_PER_USER: {{ .Values.global.defaults.maxOrgsPerUser | default "100" | quote }} + DEFAULT_ORG_MEMBER_COUNT: {{ .Values.global.defaults.maxMembersPerOrg | default "1000" | quote }} + DEFAULT_ORG_GROUP_COUNT: {{ .Values.global.defaults.maxGroupsPerOrg | default "100" | quote }} + DEFAULT_ORG_APP_COUNT: {{ .Values.global.defaults.maxAppsPerOrg | default "1000" | quote }} + DEFAULT_DEVELOPER_COUNT: {{ .Values.global.defaults.maxDevelopers | default "50" | quote }} + COMMON_WORKSPACE_MODE: {{ .Values.global.config.workspaceMode | default "SAAS" | quote }} diff --git a/deploy/helm/templates/api-service/secrets.yaml b/deploy/helm/templates/api-service/secrets.yaml index 5dd6e2f47..dfaba1e9a 100644 --- a/deploy/helm/templates/api-service/secrets.yaml +++ b/deploy/helm/templates/api-service/secrets.yaml @@ -20,6 +20,6 @@ stringData: {{- else }} MONGODB_URL: {{ .Values.mongodb.externalUrl | quote }} {{- end }} - ENCRYPTION_PASSWORD: {{ .Values.apiService.config.encryption.password | default "lowcoder.org" | quote }} - ENCRYPTION_SALT: {{ .Values.apiService.config.encryption.salt | default "lowcoder.org" | quote }} - + ENCRYPTION_PASSWORD: {{ .Values.global.config.encryption.password | default "lowcoder.org" | quote }} + ENCRYPTION_SALT: {{ .Values.global.config.encryption.salt | default "lowcoder.org" | quote }} + LOWCODER_API_KEY_SECRET: "{{ .Values.global.config.apiKeySecret }}" diff --git a/deploy/helm/templates/frontend/configMap.yaml b/deploy/helm/templates/frontend/configMap.yaml index 2bd39ebeb..6105f80af 100644 --- a/deploy/helm/templates/frontend/configMap.yaml +++ b/deploy/helm/templates/frontend/configMap.yaml @@ -11,16 +11,18 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} data: - PUID: {{ .Values.frontend.config.userId | default "9001" | quote }} - PGID: {{ .Values.frontend.config.groupId | default "9001" | quote }} - {{- if .Values.frontend.config.apiServiceUrl }} - LOWCODER_API_SERVICE_URL: {{ .Values.frontend.config.apiServiceUrl | trimSuffix "/" | quote }} + PUID: {{ .Values.global.config.userId | default "9001" | quote }} + PGID: {{ .Values.global.config.groupId | default "9001" | quote }} + {{- if .Values.global.config.apiServiceUrl }} + LOWCODER_API_SERVICE_URL: {{ .Values.global.config.apiServiceUrl | trimSuffix "/" | quote }} {{- else }} LOWCODER_API_SERVICE_URL: "http://{{ $name }}-api-service:{{ .Values.apiService.service.port }}" {{- end }} - {{- if .Values.frontend.config.nodeServiceUrl }} - LOWCODER_NODE_SERVICE_URL: {{ .Values.frontend.config.nodeServiceUrl | trimSuffix "/" | quote }} + {{- if .Values.global.config.nodeServiceUrl }} + LOWCODER_NODE_SERVICE_URL: {{ .Values.global.config.nodeServiceUrl | trimSuffix "/" | quote }} {{- else }} LOWCODER_NODE_SERVICE_URL: "http://{{ $name }}-node-service:{{ .Values.nodeService.service.port }}" {{- end }} + LOWCODER_MAX_REQUEST_SIZE: {{ .Values.global.config.maxRequestSize | default "20m" | quote }} + LOWCODER_MAX_QUERY_TIMEOUT: {{ .Values.global.config.maxQueryTimeout | default "120" | quote }} diff --git a/deploy/helm/templates/node-service/configMap.yaml b/deploy/helm/templates/node-service/configMap.yaml index ea6b26c02..f116edead 100644 --- a/deploy/helm/templates/node-service/configMap.yaml +++ b/deploy/helm/templates/node-service/configMap.yaml @@ -12,10 +12,10 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} data: - PUID: {{ .Values.nodeService.config.userId | default "9001" | quote }} - PGID: {{ .Values.nodeService.config.groupId | default "9001" | quote }} - {{- if .Values.nodeService.apiServiceUrl }} - LOWCODER_API_SERVICE_URL: {{ .Values.nodeService.apiServiceUrl | quote }} + PUID: {{ .Values.global.config.userId | default "9001" | quote }} + PGID: {{ .Values.global.config.groupId | default "9001" | quote }} + {{- if .Values.global.config.apiServiceUrl }} + LOWCODER_API_SERVICE_URL: {{ .Values.global.config.apiServiceUrl | quote }} {{- else }} LOWCODER_API_SERVICE_URL: "http://{{ $name }}-api-service:{{ .Values.apiService.service.port }}" {{- end }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index 58e29b00d..42d4e8c72 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -6,6 +6,33 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +# +# Configuration values for Lowcoder +# +global: + config: + # This setting sets workspace mode. Possible values: SAAS, ENTERPRISE + workspaceMode: SAAS + # ID of user and group runnning the service within the container + userId: 9001 + groupId: 9001 + corsAllowedDomains: "*" + enableUserSignUp: true + encryption: + password: "lowcoder.org" + salt: "lowcoder.org" + #nodeServiceUrl: + #apiServiceUrl: + apiKeySecret: "5a41b090758b39b226603177ef48d73ae9839dd458ccb7e66f7e7cc028d5a50b" + maxQueryTimeout: 120 + maxRequestSize: "20m" + defaults: + maxOrgsPerUser: 100 + maxMembersPerOrg: 1000 + maxGroupsPerOrg: 100 + maxAppsPerOrg: 1000 + maxDevelopers: 50 + # # Redis # @@ -59,15 +86,6 @@ apiService: # Overrides the image tag whose default is the chart appVersion. #tag: "latest" - config: - userId: 9001 - groupId: 9001 - corsAllowedDomains: "*" - enableUserSignUp: true - encryption: - password: "lowcoder.org" - salt: "lowcoder.org" - #nodeServiceUrl: service: type: ClusterIP @@ -93,10 +111,6 @@ nodeService: # Overrides the image tag whose default is the chart appVersion. #tag: "latest" - config: - userId: 9001 - groupId: 9001 - #apiServiceUrl: service: type: ClusterIP @@ -119,12 +133,6 @@ frontend: # Overrides the image tag whose default is the chart appVersion. #tag: "latest" - config: - userId: 9001 - groupId: 9001 - #apiServiceUrl: - #nodeServiceUrl: - service: type: NodePort port: 80 diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/query/service/QueryExecutionService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/query/service/QueryExecutionService.java index f58887d38..6a4d1fb02 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/query/service/QueryExecutionService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/query/service/QueryExecutionService.java @@ -10,6 +10,8 @@ import org.lowcoder.sdk.config.CommonConfig; import org.lowcoder.sdk.exception.BizException; import org.lowcoder.sdk.exception.PluginException; +import org.lowcoder.sdk.models.JsDatasourceConnectionConfig; +import org.lowcoder.sdk.models.Property; import org.lowcoder.sdk.models.QueryExecutionResult; import org.lowcoder.sdk.query.QueryExecutionContext; import org.lowcoder.sdk.query.QueryVisitorContext; @@ -18,6 +20,7 @@ import reactor.core.publisher.Mono; import java.time.Duration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeoutException; @@ -94,6 +97,26 @@ private Mono executeByNodeJs(Datasource datasource, Map injectOauth2Token(queryVisitorContext, context)) + .then(Mono.defer(() -> datasourcePluginClient.executeQuery(datasource.getType(), queryConfig, context, datasource.getDetailConfig()))); + } else { + return datasourcePluginClient.executeQuery(datasource.getType(), queryConfig, context, datasource.getDetailConfig()); + } + + + } + + private Mono injectOauth2Token(QueryVisitorContext queryVisitorContext, List> context) { + return queryVisitorContext.getAuthTokenMono() + .doOnNext(properties -> { + for (Property property : properties) { + context.add(Map.of("key" , property.getKey(), "value", property.getValue())); + } + }) + .then(); } } diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/models/JsDatasourceConnectionConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/models/JsDatasourceConnectionConfig.java index 96749d228..752e89d11 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/models/JsDatasourceConnectionConfig.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/models/JsDatasourceConnectionConfig.java @@ -16,6 +16,7 @@ import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; +import org.lowcoder.sdk.plugin.restapi.auth.RestApiAuthType; import org.springframework.data.annotation.Transient; import lombok.Getter; @@ -159,6 +160,20 @@ public DatasourceConnectionConfig mergeWithUpdatedConfig(DatasourceConnectionCon if (this.containsKey("extra") || jsDatasourceConnectionConfig.containsKey("extra")) { newJsDatasourceConnectionConfig.putIfAbsent("extra", ObjectUtils.firstNonNull(jsDatasourceConnectionConfig.getExtra(), this.getExtra())); } + + // for oauth handling + if(this.containsKey("authConfig")) { + if(jsDatasourceConnectionConfig.containsKey("authConfig")) { + newJsDatasourceConnectionConfig.put("authConfig", jsDatasourceConnectionConfig.get("authConfig")); + } else { + // do nothing, save empty ( this will clear db ) + } + } else { + if(jsDatasourceConnectionConfig.containsKey("authConfig")) { + newJsDatasourceConnectionConfig.put("authConfig", jsDatasourceConnectionConfig.get("authConfig")); + } + } + return newJsDatasourceConnectionConfig; } @@ -199,4 +214,18 @@ private DatasourceConnectionConfig doEncryptOrDecrypt(Function e return this; } + + public boolean isOauth2InheritFromLogin() { + if (this.get("authConfig") != null) { + return ((HashMap)this.get("authConfig")).get("type").equals(RestApiAuthType.OAUTH2_INHERIT_FROM_LOGIN.name()); + } + return false; + } + + public String getAuthId() { + if(isOauth2InheritFromLogin()) { + return ((HashMap)this.get("authConfig")).get("authId"); + } + return null; + } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/ApplicationQueryApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/ApplicationQueryApiService.java index dbf77db11..9a1242691 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/ApplicationQueryApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/ApplicationQueryApiService.java @@ -21,6 +21,7 @@ import org.lowcoder.infra.util.TupleUtils; import org.lowcoder.sdk.config.CommonConfig; import org.lowcoder.sdk.exception.BizError; +import org.lowcoder.sdk.models.JsDatasourceConnectionConfig; import org.lowcoder.sdk.models.Property; import org.lowcoder.sdk.models.QueryExecutionResult; import org.lowcoder.sdk.plugin.graphql.GraphQLDatasourceConfig; @@ -122,12 +123,18 @@ public Mono executeApplicationQuery(ServerWebExchange exch // Check if oauth inherited from login and save token if(datasource.getDetailConfig() instanceof RestApiDatasourceConfig restApiDatasourceConfig && restApiDatasourceConfig.isOauth2InheritFromLogin()) { - paramsAndHeadersInheritFromLogin = getAuthParamsAndHeadersInheritFromLogin(tuple.getT1(), ((OAuthInheritAuthConfig)restApiDatasourceConfig.getAuthConfig()).getAuthId()); + paramsAndHeadersInheritFromLogin = getAuthParamsAndHeadersInheritFromLogin(tuple.getT1(), ((OAuthInheritAuthConfig)restApiDatasourceConfig.getAuthConfig()).getAuthId(), false); } if(datasource.getDetailConfig() instanceof GraphQLDatasourceConfig graphQLDatasourceConfig && graphQLDatasourceConfig.isOauth2InheritFromLogin()) { - paramsAndHeadersInheritFromLogin = getAuthParamsAndHeadersInheritFromLogin(tuple.getT1(), ((OAuthInheritAuthConfig)graphQLDatasourceConfig.getAuthConfig()).getAuthId()); + paramsAndHeadersInheritFromLogin = getAuthParamsAndHeadersInheritFromLogin(tuple.getT1(), ((OAuthInheritAuthConfig)graphQLDatasourceConfig.getAuthConfig()).getAuthId(), false); + } + + + if(datasource.getDetailConfig() instanceof JsDatasourceConnectionConfig jsDatasourceConnectionConfig + && jsDatasourceConnectionConfig.isOauth2InheritFromLogin()) { + paramsAndHeadersInheritFromLogin = getAuthParamsAndHeadersInheritFromLogin(tuple.getT1(), jsDatasourceConnectionConfig.getAuthId(), true); } QueryVisitorContext queryVisitorContext = new QueryVisitorContext(userId, app.getOrganizationId(), port, cookies, paramsAndHeadersInheritFromLogin, commonConfig.getDisallowedHosts()); @@ -196,7 +203,7 @@ private Mono getBaseQueryFromLibraryQuery(ApplicationQuery query) { .map(LibraryQueryRecord::getQuery); } - protected Mono> getAuthParamsAndHeadersInheritFromLogin(User user, String authId) { + protected Mono> getAuthParamsAndHeadersInheritFromLogin(User user, String authId, boolean isJsQuery) { if(authId == null) { return Mono.empty(); } @@ -207,7 +214,11 @@ protected Mono> getAuthParamsAndHeadersInheritFromLogin(User user if(!activeConnectionOptional.isPresent() || activeConnectionOptional.get().getAuthConnectionAuthToken() == null) { return Mono.empty(); } - return Mono.just(Collections.singletonList(new Property("Authorization","Bearer " + activeConnectionOptional.get().getAuthConnectionAuthToken().getAccessToken(),"header"))); + if(isJsQuery) { + return Mono.just(Collections.singletonList(new Property("OAUTH_ACCESS_TOKEN",activeConnectionOptional.get().getAuthConnectionAuthToken().getAccessToken(),"header"))); + } else { + return Mono.just(Collections.singletonList(new Property("Authorization","Bearer " + activeConnectionOptional.get().getAuthConnectionAuthToken().getAccessToken(),"header"))); + } } protected void onNextOrError(QueryExecutionRequest queryExecutionRequest, QueryVisitorContext queryVisitorContext, 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 663a3be5d..a86495ac5 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 @@ -40,6 +40,7 @@ import org.lowcoder.sdk.config.CommonConfig; import org.lowcoder.sdk.exception.BizError; import org.lowcoder.sdk.exception.PluginCommonError; +import org.lowcoder.sdk.models.JsDatasourceConnectionConfig; import org.lowcoder.sdk.models.Property; import org.lowcoder.sdk.models.QueryExecutionResult; import org.lowcoder.sdk.plugin.graphql.GraphQLDatasourceConfig; @@ -263,7 +264,7 @@ public Mono executeLibraryQueryFromJs(ServerWebExchange ex Datasource datasource = tuple.getT3(); User user = tuple.getT4(); Mono> paramsAndHeadersInheritFromLogin = orgMember.isInvalid() - ? Mono.empty() : getParamsAndHeadersInheritFromLogin(user, null); + ? Mono.empty() : getParamsAndHeadersInheritFromLogin(user, null, false); QueryVisitorContext queryVisitorContext = new QueryVisitorContext(userId, orgId, port, exchange.getRequest().getCookies(), @@ -313,12 +314,18 @@ public Mono executeLibraryQuery(ServerWebExchange exchange // check if oauth inherited from login and save token if(datasource.getDetailConfig() instanceof RestApiDatasourceConfig restApiDatasourceConfig && restApiDatasourceConfig.isOauth2InheritFromLogin()) { paramsAndHeadersInheritFromLogin = getParamsAndHeadersInheritFromLogin - (user, ((OAuthInheritAuthConfig)restApiDatasourceConfig.getAuthConfig()).getAuthId()); + (user, ((OAuthInheritAuthConfig)restApiDatasourceConfig.getAuthConfig()).getAuthId(), false); } if(datasource.getDetailConfig() instanceof GraphQLDatasourceConfig graphQLDatasourceConfig && graphQLDatasourceConfig.isOauth2InheritFromLogin()) { paramsAndHeadersInheritFromLogin = getParamsAndHeadersInheritFromLogin - (user, ((OAuthInheritAuthConfig)graphQLDatasourceConfig.getAuthConfig()).getAuthId()); + (user, ((OAuthInheritAuthConfig)graphQLDatasourceConfig.getAuthConfig()).getAuthId(), false); + } + + if(datasource.getDetailConfig() instanceof JsDatasourceConnectionConfig jsDatasourceConnectionConfig + && jsDatasourceConnectionConfig.isOauth2InheritFromLogin()) { + paramsAndHeadersInheritFromLogin = getParamsAndHeadersInheritFromLogin + (user, jsDatasourceConnectionConfig.getAuthId(), true); } QueryVisitorContext queryVisitorContext = new QueryVisitorContext(userId, orgId, port, cookies, paramsAndHeadersInheritFromLogin, @@ -348,7 +355,7 @@ private Mono getBaseQuery(LibraryQueryCombineId libraryQueryCombineId .map(LibraryQueryRecord::getQuery); } - protected Mono> getParamsAndHeadersInheritFromLogin(User user, String authId) { + protected Mono> getParamsAndHeadersInheritFromLogin(User user, String authId, boolean isJsQuery) { if(authId == null) { return Mono.empty(); } @@ -359,7 +366,11 @@ protected Mono> getParamsAndHeadersInheritFromLogin(User user, St if(!activeConnectionOptional.isPresent() || activeConnectionOptional.get().getAuthConnectionAuthToken() == null) { return Mono.empty(); } - return Mono.just(Collections.singletonList(new Property("Authorization","Bearer " + activeConnectionOptional.get().getAuthConnectionAuthToken().getAccessToken(),"header"))); + if(isJsQuery) { + return Mono.just(Collections.singletonList(new Property("OAUTH_ACCESS_TOKEN",activeConnectionOptional.get().getAuthConnectionAuthToken().getAccessToken(),"header"))); + } else { + return Mono.just(Collections.singletonList(new Property("Authorization","Bearer " + activeConnectionOptional.get().getAuthConnectionAuthToken().getAccessToken(),"header"))); + } } protected void onNextOrError(QueryExecutionRequest queryExecutionRequest, QueryVisitorContext queryVisitorContext, BaseQuery baseQuery, diff --git a/server/node-service/src/plugins/openApi/index.ts b/server/node-service/src/plugins/openApi/index.ts index 32e555ca4..27f9ed950 100644 --- a/server/node-service/src/plugins/openApi/index.ts +++ b/server/node-service/src/plugins/openApi/index.ts @@ -102,7 +102,7 @@ export async function runOpenApi( try { const { parameters, requestBody } = normalizeParams(otherActionData, operation, isOas3Spec); - const securities = extractSecurityParams(dataSourceConfig.dynamicParamsConfig, definition); + let securities = extractSecurityParams(dataSourceConfig, definition); const response = await SwaggerClient.execute({ spec: definition, operationId: realOperationId, diff --git a/server/node-service/src/plugins/openApi/util.test.ts b/server/node-service/src/plugins/openApi/util.test.ts index 9000de2ca..98f6a7a53 100644 --- a/server/node-service/src/plugins/openApi/util.test.ts +++ b/server/node-service/src/plugins/openApi/util.test.ts @@ -1,7 +1,7 @@ import { extractSecurityParams, getSchemaExample, extractLevelData, parseUrl } from "./util"; test("extractSecurityParams", () => { - const params = extractSecurityParams({ "ApiKeyAuth.value": "hello", ApiKeyAuth: null }, { + const params = extractSecurityParams({"dynamicParamsConfig":{ "ApiKeyAuth.value": "hello", ApiKeyAuth: null }}, { openapi: "3.0", components: { securitySchemes: { diff --git a/server/node-service/src/plugins/openApi/util.ts b/server/node-service/src/plugins/openApi/util.ts index 827042f28..4f8a9a262 100644 --- a/server/node-service/src/plugins/openApi/util.ts +++ b/server/node-service/src/plugins/openApi/util.ts @@ -81,7 +81,8 @@ interface NormalizedParams { requestBody?: any; } -export function extractSecurityParams(config: any, spec: OpenAPI.Document) { +export function extractSecurityParams(datasourceConfig: any, spec: OpenAPI.Document) { + const config = datasourceConfig.dynamicParamsConfig; if (!config) { return {}; } @@ -96,6 +97,18 @@ export function extractSecurityParams(config: any, spec: OpenAPI.Document) { names = Object.keys(swagger2Spec.securityDefinitions || {}); } const authorized = _.pick(authData, names); + + let oauthAccessToken = datasourceConfig["OAUTH_ACCESS_TOKEN"]; + + if(oauthAccessToken) { + return { + authorized: { + OAUTH_ACCESS_TOKEN: { value: oauthAccessToken } + }, + specSecurity: [] + }; + } + return { authorized, specSecurity: spec.security }; } diff --git a/server/node-service/src/services/plugin.ts b/server/node-service/src/services/plugin.ts index 6aa9c3ee2..2dbe984f2 100644 --- a/server/node-service/src/services/plugin.ts +++ b/server/node-service/src/services/plugin.ts @@ -212,9 +212,12 @@ export async function runPluginQuery( const queryConfig = await getQueryConfig(plugin, dataSourceConfig); const action = await evalToValue(queryConfig, dsl, context, dataSourceConfig); - //forward cookies + // forward cookies context.forEach(({ key, value }) => { - if (dataSourceConfig.dynamicParamsConfig && key in dataSourceConfig.dynamicParamsConfig) { + // for oauth(inherit from login) support + if(key == "OAUTH_ACCESS_TOKEN") { + dataSourceConfig["OAUTH_ACCESS_TOKEN"] = value + } else if (dataSourceConfig.dynamicParamsConfig && key in dataSourceConfig.dynamicParamsConfig) { const valueKey = `${key}.value`; dataSourceConfig.dynamicParamsConfig[valueKey] = value[0].value } diff --git a/yarn-error.log b/yarn-error.log deleted file mode 100644 index 47e74935c..000000000 --- a/yarn-error.log +++ /dev/null @@ -1,107 +0,0 @@ -Arguments: - /usr/local/bin/node /usr/local/Cellar/yarn/1.22.0/libexec/bin/yarn.js install - -PATH: - /usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin - -Yarn version: - 1.22.0 - -Node version: - 17.4.0 - -Platform: - darwin x64 - -Trace: - SyntaxError: /Users/falkwolskyadmin/Development/Lowcoder/Development/lowcoder/package.json: Unexpected end of JSON input - at JSON.parse () - at /usr/local/Cellar/yarn/1.22.0/libexec/lib/cli.js:1625:59 - at Generator.next () - at step (/usr/local/Cellar/yarn/1.22.0/libexec/lib/cli.js:310:30) - at /usr/local/Cellar/yarn/1.22.0/libexec/lib/cli.js:321:13 - -npm manifest: - - -yarn manifest: - No manifest - -Lockfile: - # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. - # yarn lockfile v1 - - - "ansi-sequence-parser@^1.1.0": - "integrity" "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==" - "resolved" "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz" - "version" "1.1.1" - - "balanced-match@^1.0.0": - "integrity" "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - "resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - "version" "1.0.2" - - "brace-expansion@^2.0.1": - "integrity" "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==" - "resolved" "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" - "version" "2.0.1" - dependencies: - "balanced-match" "^1.0.0" - - "jsonc-parser@^3.2.0": - "integrity" "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" - "resolved" "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz" - "version" "3.2.0" - - "lunr@^2.3.9": - "integrity" "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" - "resolved" "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz" - "version" "2.3.9" - - "marked@^4.3.0": - "integrity" "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==" - "resolved" "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz" - "version" "4.3.0" - - "minimatch@^9.0.3": - "integrity" "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==" - "resolved" "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" - "version" "9.0.3" - dependencies: - "brace-expansion" "^2.0.1" - - "shiki@^0.14.1": - "integrity" "sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==" - "resolved" "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz" - "version" "0.14.5" - dependencies: - "ansi-sequence-parser" "^1.1.0" - "jsonc-parser" "^3.2.0" - "vscode-oniguruma" "^1.7.0" - "vscode-textmate" "^8.0.0" - - "typedoc@^0.25.4": - "integrity" "sha512-Du9ImmpBCw54bX275yJrxPVnjdIyJO/84co0/L9mwe0R3G4FSR6rQ09AlXVRvZEGMUg09+z/usc8mgygQ1aidA==" - "resolved" "https://registry.npmjs.org/typedoc/-/typedoc-0.25.4.tgz" - "version" "0.25.4" - dependencies: - "lunr" "^2.3.9" - "marked" "^4.3.0" - "minimatch" "^9.0.3" - "shiki" "^0.14.1" - - "typescript@4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x": - "integrity" "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==" - "resolved" "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz" - "version" "5.3.2" - - "vscode-oniguruma@^1.7.0": - "integrity" "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==" - "resolved" "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz" - "version" "1.7.0" - - "vscode-textmate@^8.0.0": - "integrity" "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==" - "resolved" "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz" - "version" "8.0.0"