Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 327e4f8

Browse files
authoredAug 27, 2024··
Merge pull request #1114 from lowcoder-org/table-summary-row
Table summary row + Inline add new row
2 parents 64d5bbd + 7f157db commit 327e4f8

21 files changed

+1056
-187
lines changed
 

‎client/packages/lowcoder/src/components/table/EditableCell.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface CellProps {
3737
candidateStatus?: { text: string; status: StatusType }[];
3838
textOverflow?: boolean;
3939
cellTooltip?: string;
40+
editMode?: string;
4041
onTableEvent?: (eventName: any) => void;
4142
}
4243

@@ -94,17 +95,19 @@ export function EditableCell<T extends JSONValue>(props: EditableCellProps<T>) {
9495
candidateTags,
9596
// tagColors
9697
candidateStatus,
98+
editMode,
9799
onTableEvent,
98100
} = props;
99101
const status = _.isNil(changeValue) ? "normal" : "toSave";
100102
const editable = editViewFn ? props.editable : false;
101103
const { isEditing, setIsEditing } = useContext(TableCellContext);
102104
const value = changeValue ?? baseValue!;
103105
const [tmpValue, setTmpValue] = useState<T | null>(value);
106+
const singleClickEdit = editMode === 'single';
104107

105108
useEffect(() => {
106109
setTmpValue(value);
107-
}, [value]);
110+
}, [JSON.stringify(value)]);
108111

109112
const onChange = useCallback(
110113
(value: T) => {
@@ -125,21 +128,27 @@ export function EditableCell<T extends JSONValue>(props: EditableCellProps<T>) {
125128
if(!_.isEqual(tmpValue, value)) {
126129
onTableEvent?.('columnEdited');
127130
}
128-
}, [dispatch, baseValue, tmpValue]);
131+
}, [dispatch, JSON.stringify(baseValue), JSON.stringify(tmpValue)]);
132+
129133
const editView = useMemo(
130134
() => editViewFn?.({ value, onChange, onChangeEnd }) ?? <></>,
131-
[editViewFn, value, onChange, onChangeEnd]
135+
[editViewFn, JSON.stringify(value), onChange, onChangeEnd]
132136
);
137+
133138
const enterEditFn = useCallback(() => {
134139
if (editable) setIsEditing(true);
135140
}, [editable]);
136141

137142
if (isEditing) {
138143
return (
139144
<>
140-
<BorderDiv />
145+
<BorderDiv className="editing-border" />
141146
<TagsContext.Provider value={candidateTags ?? []}>
142-
<StatusContext.Provider value={candidateStatus ?? []}>{editView}</StatusContext.Provider>
147+
<StatusContext.Provider value={candidateStatus ?? []}>
148+
<div className="editing-wrapper">
149+
{editView}
150+
</div>
151+
</StatusContext.Provider>
143152
</TagsContext.Provider>
144153
</>
145154
);
@@ -151,7 +160,12 @@ export function EditableCell<T extends JSONValue>(props: EditableCellProps<T>) {
151160
>
152161
{status === "toSave" && !isEditing && <EditableChip />}
153162
<CellWrapper tooltipTitle={props.cellTooltip}>
154-
{normalView}
163+
<div
164+
tabIndex={editable ? 0 : -1 }
165+
onFocus={enterEditFn}
166+
>
167+
{normalView}
168+
</div>
155169
</CellWrapper>
156170
{/* overlay on normal view to handle double click for editing */}
157171
{editable && (
@@ -164,7 +178,8 @@ export function EditableCell<T extends JSONValue>(props: EditableCellProps<T>) {
164178
width: '100%',
165179
height: '100%',
166180
}}
167-
onDoubleClick={enterEditFn}
181+
onDoubleClick={!singleClickEdit ? enterEditFn : undefined}
182+
onClick={singleClickEdit ? enterEditFn : undefined}
168183
>
169184
</div>
170185
</CellWrapper>

‎client/packages/lowcoder/src/comps/comps/selectInputComp/selectCompConstants.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,12 @@ export const SelectUIView = (
253253
inputFieldStyle: SelectStyleType;
254254
onChange: (value: any) => void;
255255
dispatch: DispatchType;
256+
autoFocus?: boolean;
256257
}
257258
) => {
258259
return <Select
259260
ref={props.viewRef}
261+
autoFocus={props.autoFocus}
260262
mode={props.mode}
261263
$inputFieldStyle={props.inputFieldStyle}
262264
$style={props.style as SelectStyleType & MultiSelectStyleType}

‎client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/ColumnNumberComp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const InputNumberWrapper = styled.div`
1414
width: 100%;
1515
border-radius: 0;
1616
background: transparent !important;
17-
padding: 0 !important;
17+
// padding: 0 !important;
1818
box-shadow: none;
1919
2020
input {

‎client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnDateComp.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ const DatePickerStyled = styled(DatePicker)<{ $open: boolean }>`
4949
top: 0.5px;
5050
display: flex;
5151
align-items: center;
52-
background: #fff;
52+
// background: #fff;
5353
padding: 0 3px;
54-
border-left: 1px solid #d7d9e0;
54+
// border-left: 1px solid #d7d9e0;
5555
}
5656
`;
5757

@@ -183,7 +183,7 @@ export const DateEdit = (props: DateEditProps) => {
183183
nextIcon={<IconNext />}
184184
superNextIcon={<IconSuperNext />}
185185
superPrevIcon={<SuperPrevIcon />}
186-
allowClear={false}
186+
allowClear={true}
187187
variant="borderless"
188188
autoFocus
189189
value={tempValue}
@@ -197,7 +197,9 @@ export const DateEdit = (props: DateEditProps) => {
197197
overflow: "hidden",
198198
}}
199199
onOpenChange={(open) => setPanelOpen(open)}
200-
onChange={(value, dateString) => props.onChange(dateString as string)}
200+
onChange={(value, dateString) => {
201+
props.onChange(dateString as string)
202+
}}
201203
onBlur={() => props.onChangeEnd()}
202204
/>
203205
</Wrapper>

‎client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnSelectComp.tsx

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,73 @@ import { StringControl } from "comps/controls/codeControl";
77
import { trans } from "i18n";
88
import { ColumnTypeCompBuilder, ColumnTypeViewFn } from "../columnTypeCompBuilder";
99
import { ColumnValueTooltip } from "../simpleColumnTypeComps";
10+
import { styled } from "styled-components";
11+
12+
const Wrapper = styled.div`
13+
display: inline-flex;
14+
align-items: center;
15+
width: 100%;
16+
height: 100%;
17+
position: absolute;
18+
top: 0;
19+
background: transparent !important;
20+
padding: 8px;
21+
22+
> div {
23+
width: 100%;
24+
height: 100%;
25+
}
26+
27+
.ant-select {
28+
height: 100%;
29+
.ant-select-selector {
30+
padding: 0 7px;
31+
height: 100%;
32+
overflow: hidden;
33+
.ant-select-selection-item {
34+
display: inline-flex;
35+
align-items: center;
36+
padding-right: 24px;
37+
}
38+
}
39+
.ant-select-arrow {
40+
height: calc(100% - 3px);
41+
width: fit-content;
42+
top: 1.5px;
43+
margin-top: 0;
44+
background-color: white;
45+
right: 1.5px;
46+
border-right: 1px solid #d7d9e0;
47+
cursor: pointer;
48+
pointer-events: auto;
49+
svg {
50+
min-width: 18px;
51+
min-height: 18px;
52+
}
53+
&:hover svg path {
54+
fill: #315efb;
55+
}
56+
}
57+
.ant-select-selector .ant-select-selection-search {
58+
left: 7px;
59+
input {
60+
height: 100%;
61+
}
62+
}
63+
&.ant-select-open {
64+
.ant-select-arrow {
65+
border-right: none;
66+
border-left: 1px solid #d7d9e0;
67+
svg g path {
68+
fill: #315efb;
69+
}
70+
}
71+
.ant-select-selection-item {
72+
opacity: 0.4;
73+
}
74+
}
75+
}
76+
`;
1077

1178
const childrenMap = {
1279
text: StringControl,
@@ -28,6 +95,8 @@ const SelectEdit = (props: SelectEditProps) => {
2895
const [currentValue, setCurrentValue] = useState(props.initialValue);
2996
return (
3097
<SelectUIView
98+
autoFocus
99+
allowClear
31100
{...defaultProps}
32101
value={currentValue}
33102
options={props.options}
@@ -67,12 +136,14 @@ export const ColumnSelectComp = (function () {
67136
)
68137
.setEditViewFn((props) => {
69138
return (
70-
<SelectEdit
71-
initialValue={props.value}
72-
options={options}
73-
onChange={props.onChange}
74-
onChangeEnd={props.onChangeEnd}
75-
/>
139+
<Wrapper>
140+
<SelectEdit
141+
initialValue={props.value}
142+
options={options}
143+
onChange={props.onChange}
144+
onChangeEnd={props.onChangeEnd}
145+
/>
146+
</Wrapper>
76147
)
77148
})
78149
.setPropertyViewFn((children) => {

‎client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnStatusComp.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const StatusEdit = (props: StatusEditPropsType) => {
4848
const defaultStatus = useContext(StatusContext);
4949
const [status, setStatus] = useState(defaultStatus);
5050
const [allOptions, setAllOptions] = useState(BadgeStatusOptions);
51-
const [open, setOpen] = useState(true);
51+
const [open, setOpen] = useState(false);
5252

5353
return (
5454
<Wrapper>
@@ -84,18 +84,20 @@ const StatusEdit = (props: StatusEditPropsType) => {
8484
value,
8585
status: status.find((item) => item.text === value)?.status || "none",
8686
});
87+
setOpen(false)
8788
}}
8889
dropdownRender={(originNode: ReactNode) => (
8990
<DropdownStyled>
9091
<ScrollBar style={{ maxHeight: "256px" }}>{originNode}</ScrollBar>
9192
</DropdownStyled>
9293
)}
9394
dropdownStyle={{ marginTop: "7px", padding: "8px 0 6px 0" }}
94-
onBlur={props.onChangeEnd}
95-
onKeyDown={(e) => {
96-
if (e.key === "Enter") {
97-
props.onChangeEnd();
98-
}
95+
onBlur={() => {
96+
props.onChangeEnd();
97+
setOpen(false);
98+
}}
99+
onFocus={() => {
100+
setOpen(true);
99101
}}
100102
onClick={() => setOpen(!open)}
101103
>

‎client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnTagsComp.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ export const Wrapper = styled.div`
9292
position: absolute;
9393
top: 0;
9494
background: transparent !important;
95+
padding: 8px;
96+
9597
> div {
9698
width: 100%;
9799
height: 100%;
@@ -147,7 +149,7 @@ export const Wrapper = styled.div`
147149
}
148150
}
149151
.ant-tag {
150-
margin-left: 20px;
152+
margin-left: 5px;
151153
}
152154
.ant-tag svg {
153155
margin-right: 4px;
@@ -159,6 +161,10 @@ export const DropdownStyled = styled.div`
159161
padding: 3px 8px;
160162
margin: 0 0 2px 8px;
161163
border-radius: 4px;
164+
165+
&.ant-select-item-option-active {
166+
background-color: #f2f7fc;
167+
}
162168
}
163169
.ant-select-item-option-content {
164170
display: flex;
@@ -193,7 +199,7 @@ const TagEdit = (props: TagEditPropsType) => {
193199
});
194200
return result;
195201
});
196-
const [open, setOpen] = useState(true);
202+
const [open, setOpen] = useState(false);
197203
return (
198204
<Wrapper>
199205
<CustomSelect
@@ -205,6 +211,7 @@ const TagEdit = (props: TagEditPropsType) => {
205211
defaultValue={props.value}
206212
style={{ width: "100%" }}
207213
open={open}
214+
allowClear={true}
208215
suffixIcon={<PackUpIcon />}
209216
onSearch={(value: string) => {
210217
if (defaultTags.findIndex((item) => item.includes(value)) < 0) {
@@ -216,18 +223,20 @@ const TagEdit = (props: TagEditPropsType) => {
216223
}}
217224
onChange={(value: string | string[]) => {
218225
props.onChange(value);
226+
setOpen(false)
219227
}}
220228
dropdownRender={(originNode: ReactNode) => (
221229
<DropdownStyled>
222230
<ScrollBar style={{ maxHeight: "256px" }}>{originNode}</ScrollBar>
223231
</DropdownStyled>
224232
)}
225233
dropdownStyle={{ marginTop: "7px", padding: "8px 0 6px 0" }}
226-
onBlur={props.onChangeEnd}
227-
onKeyDown={(e) => {
228-
if (e.key === "Enter") {
229-
props.onChangeEnd();
230-
}
234+
onFocus={() => {
235+
setOpen(true);
236+
}}
237+
onBlur={() => {
238+
props.onChangeEnd();
239+
setOpen(false);
231240
}}
232241
onClick={() => setOpen(!open)}
233242
>
@@ -259,7 +268,7 @@ export const ColumnTagsComp = (function () {
259268
tagOptionsList = props.tagColors;
260269
let value = props.changeValue ?? getBaseValue(props, dispatch);
261270
value = typeof value === "string" && value.split(",")[1] ? value.split(",") : value;
262-
const tags = _.isArray(value) ? value : [value];
271+
const tags = _.isArray(value) ? value : (value.length ? [value] : []);
263272
const view = tags.map((tag, index) => {
264273
// The actual eval value is of type number or boolean
265274
const tagText = String(tag);

‎client/packages/lowcoder/src/comps/comps/tableComp/column/simpleColumnTypeComps.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const ButtonComp = (function () {
4545
loading={props.loading}
4646
disabled={props.disabled}
4747
$buttonStyle={props.buttonType === "primary" ? style : undefined}
48+
style={{margin: 0}}
4849
>
4950
{/* prevent the button from disappearing */}
5051
{!props.text ? " " : props.text}

‎client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx

Lines changed: 138 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import styled from "styled-components";
2929
import { TextOverflowControl } from "comps/controls/textOverflowControl";
3030
import { default as Divider } from "antd/es/divider";
3131
import { ColumnValueTooltip } from "./simpleColumnTypeComps";
32+
import { SummaryColumnComp } from "./tableSummaryColumnComp";
33+
import { list } from "@lowcoder-ee/comps/generators/list";
34+
import { EMPTY_ROW_KEY } from "../tableCompView";
3235
export type Render = ReturnType<ConstructorToComp<typeof RenderComp>["getOriginalComp"]>;
3336
export const RenderComp = withSelectedMultiContext(ColumnTypeComp);
3437

@@ -145,6 +148,9 @@ export const columnChildrenMap = {
145148
linkColor: withDefault(ColorControl, "#3377ff"),
146149
linkHoverColor: withDefault(ColorControl, ""),
147150
linkActiveColor: withDefault(ColorControl, ""),
151+
summaryColumns: withDefault(list(SummaryColumnComp), [
152+
{}, {}, {}
153+
])
148154
};
149155

150156
const StyledBorderRadiusIcon = styled(IconRadius)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`;
@@ -230,7 +236,7 @@ export class ColumnComp extends ColumnInitComp {
230236
});
231237
}
232238

233-
propertyView(key: string) {
239+
propertyView(key: string, viewMode: string, summaryRowIndex: number) {
234240
const columnType = this.children.render.getSelectedComp().getComp().children.compType.getView();
235241
const initialColumns = this.children.render.getSelectedComp().getParams()?.initialColumns as OptionType[] || [];
236242
const column = this.children.render.getSelectedComp().getComp().toJsonValue();
@@ -241,138 +247,147 @@ export class ColumnComp extends ColumnInitComp {
241247
columnValue = (column.comp as any).text;
242248
}
243249

250+
const summaryColumns = this.children.summaryColumns.getView();
251+
244252
return (
245253
<>
246-
{this.children.title.propertyView({
247-
label: trans("table.columnTitle"),
248-
placeholder: this.children.dataIndex.getView(),
249-
})}
250-
{this.children.titleTooltip.propertyView({
251-
label: trans("table.columnTitleTooltip"),
252-
})}
253-
{this.children.cellTooltip.getPropertyView()}
254-
<Dropdown
255-
showSearch={true}
256-
defaultValue={columnValue}
257-
options={initialColumns}
258-
label={trans("table.dataMapping")}
259-
onChange={(value) => {
260-
// Keep the previous text value, some components do not have text, the default value is currentCell
261-
const compType = columnType;
262-
let comp: Record<string, string> = { text: value};
263-
if(columnType === 'image') {
264-
comp = { src: value };
265-
}
266-
this.children.render.dispatchChangeValueAction({
267-
compType,
268-
comp,
269-
} as any);
270-
}}
271-
/>
272-
{/* FIXME: cast type currently, return type of withContext should be corrected later */}
273-
{this.children.render.getPropertyView()}
274-
{this.children.showTitle.propertyView({
275-
label: trans("table.showTitle"),
276-
tooltip: trans("table.showTitleTooltip"),
277-
})}
278-
{ColumnTypeCompMap[columnType].canBeEditable() &&
279-
this.children.editable.propertyView({ label: trans("table.editable") })}
280-
{this.children.sortable.propertyView({
281-
label: trans("table.sortable"),
282-
})}
283-
{this.children.hide.propertyView({
284-
label: trans("prop.hide"),
285-
})}
286-
{this.children.align.propertyView({
287-
label: trans("table.align"),
288-
radioButton: true,
289-
})}
290-
{this.children.fixed.propertyView({
291-
label: trans("table.fixedColumn"),
292-
radioButton: true,
293-
})}
294-
{this.children.autoWidth.propertyView({
295-
label: trans("table.autoWidth"),
296-
radioButton: true,
297-
})}
298-
{this.children.autoWidth.getView() === "fixed" &&
299-
this.children.width.propertyView({ label: trans("prop.width") })}
300-
301-
{(columnType === 'link' || columnType === 'links') && (
254+
{viewMode === 'summary' && (
255+
summaryColumns[summaryRowIndex].propertyView('')
256+
)}
257+
{viewMode === 'normal' && (
302258
<>
259+
{this.children.title.propertyView({
260+
label: trans("table.columnTitle"),
261+
placeholder: this.children.dataIndex.getView(),
262+
})}
263+
{this.children.titleTooltip.propertyView({
264+
label: trans("table.columnTitleTooltip"),
265+
})}
266+
{this.children.cellTooltip.getPropertyView()}
267+
<Dropdown
268+
showSearch={true}
269+
defaultValue={columnValue}
270+
options={initialColumns}
271+
label={trans("table.dataMapping")}
272+
onChange={(value) => {
273+
// Keep the previous text value, some components do not have text, the default value is currentCell
274+
const compType = columnType;
275+
let comp: Record<string, string> = { text: value};
276+
if(columnType === 'image') {
277+
comp = { src: value };
278+
}
279+
this.children.render.dispatchChangeValueAction({
280+
compType,
281+
comp,
282+
} as any);
283+
}}
284+
/>
285+
{/* FIXME: cast type currently, return type of withContext should be corrected later */}
286+
{this.children.render.getPropertyView()}
287+
{this.children.showTitle.propertyView({
288+
label: trans("table.showTitle"),
289+
tooltip: trans("table.showTitleTooltip"),
290+
})}
291+
{ColumnTypeCompMap[columnType].canBeEditable() &&
292+
this.children.editable.propertyView({ label: trans("table.editable") })}
293+
{this.children.sortable.propertyView({
294+
label: trans("table.sortable"),
295+
})}
296+
{this.children.hide.propertyView({
297+
label: trans("prop.hide"),
298+
})}
299+
{this.children.align.propertyView({
300+
label: trans("table.align"),
301+
radioButton: true,
302+
})}
303+
{this.children.fixed.propertyView({
304+
label: trans("table.fixedColumn"),
305+
radioButton: true,
306+
})}
307+
{this.children.autoWidth.propertyView({
308+
label: trans("table.autoWidth"),
309+
radioButton: true,
310+
})}
311+
{this.children.autoWidth.getView() === "fixed" &&
312+
this.children.width.propertyView({ label: trans("prop.width") })}
313+
314+
{(columnType === 'link' || columnType === 'links') && (
315+
<>
316+
<Divider style={{ margin: '12px 0' }} />
317+
{controlItem({}, (
318+
<div>
319+
<b>{"Link Style"}</b>
320+
</div>
321+
))}
322+
{this.children.linkColor.propertyView({
323+
label: trans('text') // trans('style.background'),
324+
})}
325+
{this.children.linkHoverColor.propertyView({
326+
label: "Hover text", // trans('style.background'),
327+
})}
328+
{this.children.linkActiveColor.propertyView({
329+
label: "Active text", // trans('style.background'),
330+
})}
331+
</>
332+
)}
303333
<Divider style={{ margin: '12px 0' }} />
304334
{controlItem({}, (
305335
<div>
306-
<b>{"Link Style"}</b>
336+
<b>{"Column Style"}</b>
307337
</div>
308338
))}
309-
{this.children.linkColor.propertyView({
310-
label: trans('text') // trans('style.background'),
339+
{this.children.background.propertyView({
340+
label: trans('style.background'),
311341
})}
312-
{this.children.linkHoverColor.propertyView({
313-
label: "Hover text", // trans('style.background'),
342+
{columnType !== 'link' && this.children.text.propertyView({
343+
label: trans('text'),
314344
})}
315-
{this.children.linkActiveColor.propertyView({
316-
label: "Active text", // trans('style.background'),
345+
{this.children.border.propertyView({
346+
label: trans('style.border')
317347
})}
348+
{this.children.borderWidth.propertyView({
349+
label: trans('style.borderWidth'),
350+
preInputNode: <StyledBorderIcon as={BorderWidthIcon} title="" />,
351+
placeholder: '1px',
352+
})}
353+
{this.children.radius.propertyView({
354+
label: trans('style.borderRadius'),
355+
preInputNode: <StyledBorderRadiusIcon as={IconRadius} title="" />,
356+
placeholder: '3px',
357+
})}
358+
{this.children.textSize.propertyView({
359+
label: trans('style.textSize'),
360+
preInputNode: <StyledTextSizeIcon as={TextSizeIcon} title="" />,
361+
placeholder: '14px',
362+
})}
363+
{this.children.textWeight.propertyView({
364+
label: trans('style.textWeight'),
365+
preInputNode: <StyledTextWeightIcon as={TextWeightIcon} title="" />,
366+
placeholder: 'normal',
367+
})}
368+
{this.children.fontFamily.propertyView({
369+
label: trans('style.fontFamily'),
370+
preInputNode: <StyledFontFamilyIcon as={FontFamilyIcon} title="" />,
371+
placeholder: 'sans-serif',
372+
})}
373+
{this.children.fontStyle.propertyView({
374+
label: trans('style.fontStyle'),
375+
preInputNode: <StyledFontFamilyIcon as={FontFamilyIcon} title="" />,
376+
placeholder: 'normal'
377+
})}
378+
{this.children.textOverflow.getPropertyView()}
379+
{this.children.cellColor.getPropertyView()}
318380
</>
319381
)}
320-
<Divider style={{ margin: '12px 0' }} />
321-
{controlItem({}, (
322-
<div>
323-
<b>{"Column Style"}</b>
324-
</div>
325-
))}
326-
{this.children.background.propertyView({
327-
label: trans('style.background'),
328-
})}
329-
{columnType !== 'link' && this.children.text.propertyView({
330-
label: trans('text'),
331-
})}
332-
{this.children.border.propertyView({
333-
label: trans('style.border')
334-
})}
335-
{this.children.borderWidth.propertyView({
336-
label: trans('style.borderWidth'),
337-
preInputNode: <StyledBorderIcon as={BorderWidthIcon} title="" />,
338-
placeholder: '1px',
339-
})}
340-
{this.children.radius.propertyView({
341-
label: trans('style.borderRadius'),
342-
preInputNode: <StyledBorderRadiusIcon as={IconRadius} title="" />,
343-
placeholder: '3px',
344-
})}
345-
{this.children.textSize.propertyView({
346-
label: trans('style.textSize'),
347-
preInputNode: <StyledTextSizeIcon as={TextSizeIcon} title="" />,
348-
placeholder: '14px',
349-
})}
350-
{this.children.textWeight.propertyView({
351-
label: trans('style.textWeight'),
352-
preInputNode: <StyledTextWeightIcon as={TextWeightIcon} title="" />,
353-
placeholder: 'normal',
354-
})}
355-
{this.children.fontFamily.propertyView({
356-
label: trans('style.fontFamily'),
357-
preInputNode: <StyledFontFamilyIcon as={FontFamilyIcon} title="" />,
358-
placeholder: 'sans-serif',
359-
})}
360-
{this.children.fontStyle.propertyView({
361-
label: trans('style.fontStyle'),
362-
preInputNode: <StyledFontFamilyIcon as={FontFamilyIcon} title="" />,
363-
placeholder: 'normal'
364-
})}
365-
{this.children.textOverflow.getPropertyView()}
366-
{this.children.cellColor.getPropertyView()}
367382
</>
368383
);
369384
}
370385

371386
getChangeSet() {
372387
const dataIndex = this.children.dataIndex.getView();
373-
const changeSet = _.mapValues(this.children.render.getMap(), (value) =>
374-
value.getComp().children.comp.children.changeValue.getView()
375-
);
388+
const changeSet = _.mapValues(this.children.render.getMap(), (value) =>{
389+
return value.getComp().children.comp.children.changeValue.getView()
390+
});
376391
return { [dataIndex]: changeSet };
377392
}
378393

@@ -389,6 +404,15 @@ export class ColumnComp extends ColumnInitComp {
389404
);
390405
}
391406

407+
dispatchClearInsertSet() {
408+
const renderMap = this.children.render.getMap();
409+
const insertMapKeys = Object.keys(renderMap).filter(key => key.startsWith(EMPTY_ROW_KEY));
410+
insertMapKeys.forEach(key => {
411+
const render = renderMap[key];
412+
render.getComp().children.comp.children.changeValue.dispatchChangeValueAction(null);
413+
});
414+
}
415+
392416
static setSelectionAction(key: string) {
393417
return wrapChildAction("render", RenderComp.setSelectionAction(key));
394418
}

‎client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnListComp.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { shallowEqual } from "react-redux";
1818
import { JSONObject, JSONValue } from "util/jsonTypes";
1919
import { lastValueIfEqual } from "util/objectUtils";
20+
import { EMPTY_ROW_KEY } from "../tableCompView";
2021

2122
/**
2223
* column list
@@ -70,14 +71,17 @@ export class ColumnListComp extends ColumnListTmpComp {
7071
return super.reduce(action);
7172
}
7273

73-
getChangeSet() {
74+
getChangeSet(filterNewRowsChange?: boolean) {
7475
const changeSet: Record<string, Record<string, JSONValue>> = {};
7576
const columns = this.getView();
7677
columns.forEach((column) => {
7778
const columnChangeSet = column.getChangeSet();
7879
Object.keys(columnChangeSet).forEach((dataIndex) => {
7980
Object.keys(columnChangeSet[dataIndex]).forEach((key) => {
80-
if (!_.isNil(columnChangeSet[dataIndex][key])) {
81+
const includeChange = filterNewRowsChange
82+
? key.startsWith(EMPTY_ROW_KEY)
83+
: !key.startsWith(EMPTY_ROW_KEY);
84+
if (!_.isNil(columnChangeSet[dataIndex][key]) && includeChange) {
8185
if (!changeSet[key]) changeSet[key] = {};
8286
changeSet[key][dataIndex] = columnChangeSet[dataIndex][key];
8387
}
@@ -92,6 +96,11 @@ export class ColumnListComp extends ColumnListTmpComp {
9296
columns.forEach((column) => column.dispatchClearChangeSet());
9397
}
9498

99+
dispatchClearInsertSet() {
100+
const columns = this.getView();
101+
columns.forEach((column) => column.dispatchClearInsertSet());
102+
}
103+
95104
/**
96105
* If the table data changes, call this method to trigger the action
97106
*/
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { RadiusControl, StringControl } from "comps/controls/codeControl";
2+
import { HorizontalAlignmentControl } from "comps/controls/dropdownControl";
3+
import { MultiCompBuilder, valueComp, withDefault } from "comps/generators";
4+
import { withSelectedMultiContext } from "comps/generators/withSelectedMultiContext";
5+
import { trans } from "i18n";
6+
import _ from "lodash";
7+
import {
8+
changeChildAction,
9+
CompAction,
10+
ConstructorToComp,
11+
deferAction,
12+
fromRecord,
13+
withFunction,
14+
wrapChildAction,
15+
} from "lowcoder-core";
16+
import { IconRadius, TextSizeIcon, FontFamilyIcon, TextWeightIcon, controlItem } from "lowcoder-design";
17+
import { ColumnTypeComp } from "./columnTypeComp";
18+
import { ColorControl } from "comps/controls/colorControl";
19+
import styled from "styled-components";
20+
import { TextOverflowControl } from "comps/controls/textOverflowControl";
21+
import { default as Divider } from "antd/es/divider";
22+
export type Render = ReturnType<ConstructorToComp<typeof RenderComp>["getOriginalComp"]>;
23+
export const RenderComp = withSelectedMultiContext(ColumnTypeComp);
24+
25+
export const columnChildrenMap = {
26+
cellTooltip: StringControl,
27+
// a custom column or a data column
28+
isCustom: valueComp<boolean>(false),
29+
// If it is a data column, it must be the name of the column and cannot be duplicated as a react key
30+
dataIndex: valueComp<string>(""),
31+
render: RenderComp,
32+
align: HorizontalAlignmentControl,
33+
background: withDefault(ColorControl, ""),
34+
margin: withDefault(RadiusControl, ""),
35+
text: withDefault(ColorControl, ""),
36+
border: withDefault(ColorControl, ""),
37+
radius: withDefault(RadiusControl, ""),
38+
textSize: withDefault(RadiusControl, ""),
39+
textWeight: withDefault(StringControl, "normal"),
40+
fontFamily: withDefault(StringControl, "sans-serif"),
41+
fontStyle: withDefault(StringControl, 'normal'),
42+
cellColor: StringControl,
43+
textOverflow: withDefault(TextOverflowControl, "ellipsis"),
44+
linkColor: withDefault(ColorControl, "#3377ff"),
45+
linkHoverColor: withDefault(ColorControl, ""),
46+
linkActiveColor: withDefault(ColorControl, ""),
47+
};
48+
49+
const StyledBorderRadiusIcon = styled(IconRadius)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`;
50+
const StyledTextSizeIcon = styled(TextSizeIcon)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`;
51+
const StyledFontFamilyIcon = styled(FontFamilyIcon)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`;
52+
const StyledTextWeightIcon = styled(TextWeightIcon)` width: 24px; margin: 0 8px 0 -3px; padding: 3px;`;
53+
54+
/**
55+
* export for test.
56+
* Put it here temporarily to avoid circular dependencies
57+
*/
58+
const ColumnInitComp = new MultiCompBuilder(columnChildrenMap, (props, dispatch) => {
59+
return {
60+
...props,
61+
};
62+
})
63+
.setPropertyViewFn(() => <></>)
64+
.build();
65+
66+
export class SummaryColumnComp extends ColumnInitComp {
67+
override reduce(action: CompAction) {
68+
const comp = super.reduce(action);
69+
return comp;
70+
}
71+
72+
override getView() {
73+
const superView = super.getView();
74+
const columnType = this.children.render.getSelectedComp().getComp().children.compType.getView();
75+
return {
76+
...superView,
77+
columnType,
78+
};
79+
}
80+
81+
exposingNode() {
82+
const dataIndexNode = this.children.dataIndex.exposingNode();
83+
84+
const renderNode = withFunction(this.children.render.node(), (render) => ({
85+
wrap: render.__comp__.wrap,
86+
map: _.mapValues(render.__map__, (value) => value.comp),
87+
}));
88+
return fromRecord({
89+
dataIndex: dataIndexNode,
90+
render: renderNode,
91+
});
92+
}
93+
94+
propertyView(key: string) {
95+
const columnType = this.children.render.getSelectedComp().getComp().children.compType.getView();
96+
const column = this.children.render.getSelectedComp().getComp().toJsonValue();
97+
let columnValue = '{{currentCell}}';
98+
if (column.comp?.hasOwnProperty('src')) {
99+
columnValue = (column.comp as any).src;
100+
} else if (column.comp?.hasOwnProperty('text')) {
101+
columnValue = (column.comp as any).text;
102+
}
103+
104+
return (
105+
<>
106+
{this.children.cellTooltip.propertyView({
107+
label: trans("table.columnTooltip"),
108+
})}
109+
{this.children.render.getPropertyView()}
110+
{this.children.align.propertyView({
111+
label: trans("table.align"),
112+
radioButton: true,
113+
})}
114+
{(columnType === 'link' || columnType === 'links') && (
115+
<>
116+
<Divider style={{ margin: '12px 0' }} />
117+
{controlItem({}, (
118+
<div>
119+
<b>{"Link Style"}</b>
120+
</div>
121+
))}
122+
{this.children.linkColor.propertyView({
123+
label: trans('text')
124+
})}
125+
{this.children.linkHoverColor.propertyView({
126+
label: "Hover text",
127+
})}
128+
{this.children.linkActiveColor.propertyView({
129+
label: "Active text",
130+
})}
131+
</>
132+
)}
133+
<Divider style={{ margin: '12px 0' }} />
134+
{controlItem({}, (
135+
<div>
136+
<b>{"Column Style"}</b>
137+
</div>
138+
))}
139+
{this.children.background.propertyView({
140+
label: trans('style.background'),
141+
})}
142+
{columnType !== 'link' && this.children.text.propertyView({
143+
label: trans('text'),
144+
})}
145+
{this.children.border.propertyView({
146+
label: trans('style.border')
147+
})}
148+
{this.children.radius.propertyView({
149+
label: trans('style.borderRadius'),
150+
preInputNode: <StyledBorderRadiusIcon as={IconRadius} title="" />,
151+
placeholder: '3px',
152+
})}
153+
{this.children.textSize.propertyView({
154+
label: trans('style.textSize'),
155+
preInputNode: <StyledTextSizeIcon as={TextSizeIcon} title="" />,
156+
placeholder: '14px',
157+
})}
158+
{this.children.textWeight.propertyView({
159+
label: trans('style.textWeight'),
160+
preInputNode: <StyledTextWeightIcon as={TextWeightIcon} title="" />,
161+
placeholder: 'normal',
162+
})}
163+
{this.children.fontFamily.propertyView({
164+
label: trans('style.fontFamily'),
165+
preInputNode: <StyledFontFamilyIcon as={FontFamilyIcon} title="" />,
166+
placeholder: 'sans-serif',
167+
})}
168+
{this.children.fontStyle.propertyView({
169+
label: trans('style.fontStyle'),
170+
preInputNode: <StyledFontFamilyIcon as={FontFamilyIcon} title="" />,
171+
placeholder: 'normal'
172+
})}
173+
{/* {this.children.textOverflow.getPropertyView()} */}
174+
{this.children.cellColor.propertyView({
175+
label: trans("table.cellColor"),
176+
})}
177+
</>
178+
);
179+
}
180+
181+
getChangeSet() {
182+
const dataIndex = this.children.dataIndex.getView();
183+
const changeSet = _.mapValues(this.children.render.getMap(), (value) =>
184+
value.getComp().children.comp.children.changeValue.getView()
185+
);
186+
return { [dataIndex]: changeSet };
187+
}
188+
189+
dispatchClearChangeSet() {
190+
this.children.render.dispatch(
191+
deferAction(
192+
RenderComp.forEachAction(
193+
wrapChildAction(
194+
"comp",
195+
wrapChildAction("comp", changeChildAction("changeValue", null, false))
196+
)
197+
)
198+
)
199+
);
200+
}
201+
202+
static setSelectionAction(key: string) {
203+
return wrapChildAction("render", RenderComp.setSelectionAction(key));
204+
}
205+
}

‎client/packages/lowcoder/src/comps/comps/tableComp/tableComp.tsx

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { tableDataRowExample } from "comps/comps/tableComp/column/tableColumnListComp";
22
import { getPageSize } from "comps/comps/tableComp/paginationControl";
3-
import { TableCompView } from "comps/comps/tableComp/tableCompView";
3+
import { EMPTY_ROW_KEY, TableCompView } from "comps/comps/tableComp/tableCompView";
44
import { TableFilter } from "comps/comps/tableComp/tableToolbarComp";
55
import {
66
columnHide,
@@ -40,7 +40,11 @@ import {
4040
deferAction,
4141
executeQueryAction,
4242
fromRecord,
43+
FunctionNode,
44+
Node,
4345
onlyEvalAction,
46+
RecordNode,
47+
RecordNodeToValue,
4448
routeByNameAction,
4549
withFunction,
4650
wrapChildAction,
@@ -53,7 +57,7 @@ import { getSelectedRowKeys } from "./selectionControl";
5357
import { compTablePropertyView } from "./tablePropertyView";
5458
import { RowColorComp, RowHeightComp, TableChildrenView, TableInitComp } from "./tableTypes";
5559

56-
import { useContext } from "react";
60+
import { useContext, useState } from "react";
5761
import { EditorContext } from "comps/editorState";
5862

5963
export class TableImplComp extends TableInitComp implements IContainer {
@@ -386,12 +390,11 @@ export class TableImplComp extends TableInitComp implements IContainer {
386390
)[0];
387391
}
388392

389-
changeSetNode() {
390-
const nodes = {
391-
dataIndexes: this.children.columns.getColumnsNode("dataIndex"),
392-
renders: this.children.columns.getColumnsNode("render"),
393-
};
394-
const resNode = withFunction(fromRecord(nodes), (input) => {
393+
private getUpsertSetResNode(
394+
nodes: Record<string, RecordNode<Record<string, Node<any>>>>,
395+
filterNewRows?: boolean,
396+
) {
397+
return withFunction(fromRecord(nodes), (input) => {
395398
// merge input.dataIndexes and input.withParams into one structure
396399
const dataIndexRenderDict = _(input.dataIndexes)
397400
.mapValues((dataIndex, idx) => input.renders[idx])
@@ -401,26 +404,45 @@ export class TableImplComp extends TableInitComp implements IContainer {
401404
_.forEach(dataIndexRenderDict, (render, dataIndex) => {
402405
_.forEach(render[MAP_KEY], (value, key) => {
403406
const changeValue = (value.comp as any).comp.changeValue;
404-
if (!_.isNil(changeValue)) {
407+
const includeRecord = (filterNewRows && key.startsWith(EMPTY_ROW_KEY)) || (!filterNewRows && !key.startsWith(EMPTY_ROW_KEY));
408+
if (!_.isNil(changeValue) && includeRecord) {
405409
if (!record[key]) record[key] = {};
406410
record[key][dataIndex] = changeValue;
407411
}
408412
});
409413
});
410414
return record;
411415
});
416+
}
417+
418+
changeSetNode() {
419+
const nodes = {
420+
dataIndexes: this.children.columns.getColumnsNode("dataIndex"),
421+
renders: this.children.columns.getColumnsNode("render"),
422+
};
423+
424+
const resNode = this.getUpsertSetResNode(nodes);
412425
return lastValueIfEqual(this, "changeSetNode", [resNode, nodes] as const, (a, b) =>
413426
shallowEqual(a[1], b[1])
414427
)[0];
415428
}
416429

417-
toUpdateRowsNode() {
430+
insertSetNode() {
418431
const nodes = {
419-
oriDisplayData: this.oriDisplayDataNode(),
420-
indexes: this.displayDataIndexesNode(),
421-
changeSet: this.changeSetNode(),
432+
dataIndexes: this.children.columns.getColumnsNode("dataIndex"),
433+
renders: this.children.columns.getColumnsNode("render"),
422434
};
423-
const resNode = withFunction(fromRecord(nodes), (input) => {
435+
436+
const resNode = this.getUpsertSetResNode(nodes, true);
437+
return lastValueIfEqual(this, "insertSetNode", [resNode, nodes] as const, (a, b) =>
438+
shallowEqual(a[1], b[1])
439+
)[0];
440+
}
441+
442+
private getToUpsertRowsResNodes(
443+
nodes: Record<string, FunctionNode<any, any>>
444+
) {
445+
return withFunction(fromRecord(nodes), (input) => {
424446
const res = _(input.changeSet)
425447
.map((changeValues, oriIndex) => {
426448
const idx = input.indexes[oriIndex];
@@ -431,11 +453,34 @@ export class TableImplComp extends TableInitComp implements IContainer {
431453
// console.info("toUpdateRowsNode. input: ", input, " res: ", res);
432454
return res;
433455
});
456+
}
457+
458+
toUpdateRowsNode() {
459+
const nodes = {
460+
oriDisplayData: this.oriDisplayDataNode(),
461+
indexes: this.displayDataIndexesNode(),
462+
changeSet: this.changeSetNode(),
463+
};
464+
465+
const resNode = this.getToUpsertRowsResNodes(nodes);
434466
return lastValueIfEqual(this, "toUpdateRowsNode", [resNode, nodes] as const, (a, b) =>
435467
shallowEqual(a[1], b[1])
436468
)[0];
437469
}
438470

471+
toInsertRowsNode() {
472+
const nodes = {
473+
oriDisplayData: this.oriDisplayDataNode(),
474+
indexes: this.displayDataIndexesNode(),
475+
changeSet: this.insertSetNode(),
476+
};
477+
478+
const resNode = this.getToUpsertRowsResNodes(nodes);
479+
return lastValueIfEqual(this, "toInsertRowsNode", [resNode, nodes] as const, (a, b) =>
480+
shallowEqual(a[1], b[1])
481+
)[0];
482+
}
483+
439484
columnAggrNode() {
440485
const nodes = {
441486
oriDisplayData: this.oriDisplayDataNode(),
@@ -458,6 +503,7 @@ export class TableImplComp extends TableInitComp implements IContainer {
458503
}
459504

460505
let TableTmpComp = withViewFn(TableImplComp, (comp) => {
506+
const [emptyRows, setEmptyRows] = useState([]);
461507
return (
462508
<HidableView hidden={comp.children.hidden.getView()}>
463509
<TableCompView
@@ -672,6 +718,14 @@ export const TableComp = withExposingConfigs(TableTmpComp, [
672718
(input) => input.changeSet,
673719
trans("table.changeSetDesc")
674720
),
721+
new CompDepsConfig(
722+
"insertSet",
723+
(comp) => ({
724+
insertSet: comp.insertSetNode(),
725+
}),
726+
(input) => input.insertSet,
727+
trans("table.changeSetDesc")
728+
),
675729
new CompDepsConfig(
676730
"toUpdateRows",
677731
(comp) => ({
@@ -682,6 +736,16 @@ export const TableComp = withExposingConfigs(TableTmpComp, [
682736
},
683737
trans("table.toUpdateRowsDesc")
684738
),
739+
new CompDepsConfig(
740+
"toInsertRows",
741+
(comp) => ({
742+
toInsertRows: comp.toInsertRowsNode(),
743+
}),
744+
(input) => {
745+
return input.toInsertRows;
746+
},
747+
trans("table.toUpdateRowsDesc")
748+
),
685749
new DepsConfig(
686750
"pageNo",
687751
(children) => {

‎client/packages/lowcoder/src/comps/comps/tableComp/tableCompView.tsx

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { RowColorViewType, RowHeightViewType, TableEventOptionValues } from "com
55
import {
66
COL_MIN_WIDTH,
77
COLUMN_CHILDREN_KEY,
8+
ColumnsAggrData,
89
columnsToAntdFormat,
910
CustomColumnType,
1011
OB_ROW_ORI_INDEX,
@@ -39,10 +40,13 @@ import { SlotConfigContext } from "comps/controls/slotControl";
3940
import { EmptyContent } from "pages/common/styledComponent";
4041
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
4142
import { ReactRef, ResizeHandleAxis } from "layout/gridLayoutPropTypes";
42-
import { CellColorViewType } from "./column/tableColumnComp";
43+
import { CellColorViewType, ColumnComp } from "./column/tableColumnComp";
4344
import { defaultTheme } from "@lowcoder-ee/constants/themeConstants";
4445
import { childrenToProps } from "@lowcoder-ee/comps/generators/multi";
4546
import { getVerticalMargin } from "@lowcoder-ee/util/cssUtil";
47+
import { TableSummary } from "./tableSummaryComp";
48+
49+
export const EMPTY_ROW_KEY = 'empty_row';
4650

4751
function genLinerGradient(color: string) {
4852
return `linear-gradient(${color}, ${color})`;
@@ -321,15 +325,6 @@ const TableWrapper = styled.div<{
321325
border-top-right-radius: 0px;
322326
}
323327
}
324-
325-
// hide the bottom border of the last row
326-
${(props) =>
327-
props.$toolbarPosition !== "below" &&
328-
`
329-
tbody > tr:last-child > td {
330-
border-bottom: unset;
331-
}
332-
`}
333328
}
334329
335330
.ant-table-expanded-row-fixed:after {
@@ -376,16 +371,19 @@ const TableTd = styled.td<{
376371
border-radius: ${(props) => props.$style.radius};
377372
padding: 0 !important;
378373
379-
> div {
380-
margin: ${(props) => props.$style.margin};
374+
> div:not(.editing-border, .editing-wrapper),
375+
.editing-wrapper .ant-input,
376+
.editing-wrapper .ant-input-number,
377+
.editing-wrapper .ant-picker {
378+
margin: ${(props) => props.$isEditing ? '0px' : props.$style.margin};
381379
color: ${(props) => props.$style.text};
382380
font-weight: ${(props) => props.$style.textWeight};
383381
font-family: ${(props) => props.$style.fontFamily};
384382
overflow: hidden;
385383
${(props) => props.$tableSize === 'small' && `
386384
padding: 1px 8px;
387385
font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '14px !important' : props.$style.textSize + ' !important'};
388-
font-style:${props.$style.fontStyle} !important;
386+
font-style:${props.$style.fontStyle} !important;
389387
min-height: ${props.$style.rowHeight || '14px'};
390388
line-height: 20px;
391389
${!props.$autoHeight && `
@@ -596,7 +594,7 @@ function TableCellView(props: {
596594
</TableTd>
597595
);
598596
}
599-
597+
600598
return (
601599
<TableCellContext.Provider value={{ isEditing: editing, setIsEditing: setEditing }}>
602600
{tdView}
@@ -714,11 +712,25 @@ function ResizeableTable<RecordType extends object>(props: CustomTableProps<Reco
714712

715713
ResizeableTable.whyDidYouRender = true;
716714

715+
const createNewEmptyRow = (
716+
rowIndex: number,
717+
columnsAggrData: ColumnsAggrData,
718+
) => {
719+
const emptyRowData: RecordType = {
720+
[OB_ROW_ORI_INDEX]: `${EMPTY_ROW_KEY}_${rowIndex}`,
721+
};
722+
Object.keys(columnsAggrData).forEach(columnKey => {
723+
emptyRowData[columnKey] = '';
724+
});
725+
return emptyRowData;
726+
}
727+
717728
export function TableCompView(props: {
718729
comp: InstanceType<typeof TableImplComp>;
719730
onRefresh: (allQueryNames: Array<string>, setLoading: (loading: boolean) => void) => void;
720731
onDownload: (fileName: string) => void;
721732
}) {
733+
const [emptyRowsMap, setEmptyRowsMap] = useState<Record<string, RecordType>>({});
722734
const editorState = useContext(EditorContext);
723735
const { width, ref } = useResizeDetector({
724736
refreshMode: "debounce",
@@ -741,15 +753,21 @@ export function TableCompView(props: {
741753
const visibleResizables = compChildren.visibleResizables.getView();
742754
const showHRowGridBorder = compChildren.showHRowGridBorder.getView();
743755
const columnsStyle = compChildren.columnsStyle.getView();
756+
const summaryRowStyle = compChildren.summaryRowStyle.getView();
744757
const changeSet = useMemo(() => compChildren.columns.getChangeSet(), [compChildren.columns]);
745-
const hasChange = useMemo(() => !_.isEmpty(changeSet), [changeSet]);
758+
const insertSet = useMemo(() => compChildren.columns.getChangeSet(true), [compChildren.columns]);
759+
const hasChange = useMemo(() => !_.isEmpty(changeSet) || !_.isEmpty(insertSet), [changeSet, insertSet]);
746760
const columns = useMemo(() => compChildren.columns.getView(), [compChildren.columns]);
747761
const columnViews = useMemo(() => columns.map((c) => c.getView()), [columns]);
748762
const data = comp.filterData;
749763
const sort = useMemo(() => compChildren.sort.getView(), [compChildren.sort]);
750764
const toolbar = useMemo(() => compChildren.toolbar.getView(), [compChildren.toolbar]);
765+
const showSummary = useMemo(() => compChildren.showSummary.getView(), [compChildren.showSummary]);
766+
const summaryRows = useMemo(() => compChildren.summaryRows.getView(), [compChildren.summaryRows]);
767+
const inlineAddNewRow = useMemo(() => compChildren.inlineAddNewRow.getView(), [compChildren.inlineAddNewRow]);
751768
const pagination = useMemo(() => compChildren.pagination.getView(), [compChildren.pagination]);
752769
const size = useMemo(() => compChildren.size.getView(), [compChildren.size]);
770+
const editModeClicks = useMemo(() => compChildren.editModeClicks.getView(), [compChildren.editModeClicks]);
753771
const onEvent = useMemo(() => compChildren.onEvent.getView(), [compChildren.onEvent]);
754772
const dynamicColumn = compChildren.dynamicColumn.getView();
755773
const dynamicColumnConfig = useMemo(
@@ -768,6 +786,7 @@ export function TableCompView(props: {
768786
dynamicColumn,
769787
dynamicColumnConfig,
770788
columnsAggrData,
789+
editModeClicks,
771790
onEvent,
772791
),
773792
[
@@ -778,13 +797,78 @@ export function TableCompView(props: {
778797
dynamicColumn,
779798
dynamicColumnConfig,
780799
columnsAggrData,
800+
editModeClicks,
781801
]
782802
);
803+
783804
const supportChildren = useMemo(
784805
() => supportChildrenTree(compChildren.data.getView()),
785806
[compChildren.data]
786807
);
787808

809+
const updateEmptyRows = useCallback(() => {
810+
if (!inlineAddNewRow) {
811+
setEmptyRowsMap({})
812+
setTimeout(() => compChildren.columns.dispatchClearInsertSet());
813+
return;
814+
}
815+
816+
let emptyRows: Record<string, RecordType> = {...emptyRowsMap};
817+
const existingRowsKeys = Object.keys(emptyRows);
818+
const existingRowsCount = existingRowsKeys.length;
819+
const updatedRowsKeys = Object.keys(insertSet).filter(
820+
key => key.startsWith(EMPTY_ROW_KEY)
821+
);
822+
const updatedRowsCount = updatedRowsKeys.length;
823+
const removedRowsKeys = existingRowsKeys.filter(
824+
x => !updatedRowsKeys.includes(x)
825+
);
826+
827+
if (removedRowsKeys.length === existingRowsCount) {
828+
const newRowIndex = 0;
829+
const newRowKey = `${EMPTY_ROW_KEY}_${newRowIndex}`;
830+
setEmptyRowsMap({
831+
[newRowKey]: createNewEmptyRow(newRowIndex, columnsAggrData)
832+
});
833+
const ele = document.querySelector<HTMLElement>(`[data-row-key=${newRowKey}]`);
834+
if (ele) {
835+
ele.style.display = '';
836+
}
837+
return;
838+
}
839+
840+
removedRowsKeys.forEach(rowKey => {
841+
if (
842+
rowKey === existingRowsKeys[existingRowsCount - 1]
843+
|| rowKey === existingRowsKeys[existingRowsCount - 2]
844+
) {
845+
delete emptyRows[rowKey];
846+
} else {
847+
const ele = document.querySelector<HTMLElement>(`[data-row-key=${rowKey}]`);
848+
if (ele) {
849+
ele.style.display = 'none';
850+
}
851+
}
852+
})
853+
const lastRowKey = updatedRowsCount ? updatedRowsKeys[updatedRowsCount - 1] : '';
854+
const lastRowIndex = lastRowKey ? parseInt(lastRowKey.replace(`${EMPTY_ROW_KEY}_`, '')) : -1;
855+
856+
const newRowIndex = lastRowIndex + 1;
857+
const newRowKey = `${EMPTY_ROW_KEY}_${newRowIndex}`;
858+
emptyRows[newRowKey] = createNewEmptyRow(newRowIndex, columnsAggrData);
859+
setEmptyRowsMap(emptyRows);
860+
}, [
861+
inlineAddNewRow,
862+
JSON.stringify(insertSet),
863+
setEmptyRowsMap,
864+
createNewEmptyRow,
865+
]);
866+
867+
useEffect(() => {
868+
updateEmptyRows();
869+
}, [updateEmptyRows]);
870+
871+
788872
const pageDataInfo = useMemo(() => {
789873
// Data pagination
790874
let pagedData = data;
@@ -799,6 +883,7 @@ export function TableCompView(props: {
799883
}
800884
pagedData = pagedData.slice(offset, offset + pagination.pageSize);
801885
}
886+
802887
return {
803888
total: total,
804889
current: current,
@@ -842,11 +927,30 @@ export function TableCompView(props: {
842927
}}
843928
hasChange={hasChange}
844929
onSaveChanges={() => handleChangeEvent("saveChanges")}
845-
onCancelChanges={() => handleChangeEvent("cancelChanges")}
930+
onCancelChanges={() => {
931+
handleChangeEvent("cancelChanges");
932+
if (inlineAddNewRow) {
933+
setEmptyRowsMap({});
934+
}
935+
}}
846936
onEvent={onEvent}
847937
/>
848938
);
849939

940+
const summaryView = () => {
941+
if (!showSummary) return undefined;
942+
return (
943+
<TableSummary
944+
tableSize={size}
945+
istoolbarPositionBelow={toolbar.position === "below"}
946+
expandableRows={Boolean(expansion.expandModalView)}
947+
summaryRows={parseInt(summaryRows)}
948+
columns={columns}
949+
summaryRowStyle={summaryRowStyle}
950+
/>
951+
);
952+
}
953+
850954
if (antdColumns.length === 0) {
851955
return <EmptyContent text={trans("table.emptyColumns")} />;
852956
}
@@ -896,6 +1000,7 @@ export function TableCompView(props: {
8961000
}
8971001
}
8981002
}}
1003+
// rowKey={OB_ROW_ORI_INDEX}
8991004
rowColorFn={compChildren.rowColor.getView() as any}
9001005
rowHeightFn={compChildren.rowHeight.getView() as any}
9011006
{...compChildren.selection.getView()(onEvent)}
@@ -908,7 +1013,7 @@ export function TableCompView(props: {
9081013
columnsStyle={columnsStyle}
9091014
viewModeResizable={compChildren.viewModeResizable.getView()}
9101015
visibleResizables={compChildren.visibleResizables.getView()}
911-
dataSource={pageDataInfo.data}
1016+
dataSource={pageDataInfo.data.concat(Object.values(emptyRowsMap))}
9121017
size={compChildren.size.getView()}
9131018
rowAutoHeight={rowAutoHeight}
9141019
tableLayout="fixed"
@@ -925,8 +1030,8 @@ export function TableCompView(props: {
9251030
dataIndex: dataIndex,
9261031
});
9271032
}}
1033+
summary={summaryView}
9281034
/>
929-
9301035
<SlotConfigContext.Provider value={{ modalWidth: width && Math.max(width, 300) }}>
9311036
{expansion.expandModalView}
9321037
</SlotConfigContext.Provider>

‎client/packages/lowcoder/src/comps/comps/tableComp/tableDynamicColumn.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const expectColumn = (
4545
comp.children.dynamicColumn.getView(),
4646
dynamicColumnConfig,
4747
comp.columnAggrData,
48+
comp.children.editModeClicks.getView(),
4849
onEvent,
4950
);
5051
expect(columnViews.length).toBeGreaterThanOrEqual(antdColumns.length);

‎client/packages/lowcoder/src/comps/comps/tableComp/tablePropertyView.tsx

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import React, { useMemo, useState } from "react";
3333
import { GreyTextColor } from "constants/style";
3434
import { alignOptions } from "comps/controls/dropdownControl";
3535
import { ColumnTypeCompMap } from "comps/comps/tableComp/column/columnTypeComp";
36-
import { changeChildAction } from "lowcoder-core";
36+
import Segmented from "antd/es/segmented";
3737

3838
const InsertDiv = styled.div`
3939
display: flex;
@@ -104,6 +104,34 @@ const ColumnBatchOptionWrapper = styled.div`
104104
font-size: 13px;
105105
`;
106106

107+
type ViewOptionType = "normal" | "summary";
108+
109+
const summaryRowOptions = [
110+
{
111+
label: "Row 1",
112+
value: 0,
113+
},
114+
{
115+
label: "Row 2",
116+
value: 1,
117+
},
118+
{
119+
label: "Row 3",
120+
value: 2,
121+
},
122+
];
123+
124+
const columnViewOptions = [
125+
{
126+
label: "Normal",
127+
value: "normal",
128+
},
129+
{
130+
label: "Summary",
131+
value: "summary",
132+
},
133+
];
134+
107135
const columnFilterOptions = [
108136
{ label: trans("table.allColumn"), value: "all" },
109137
{ label: trans("table.visibleColumn"), value: "visible" },
@@ -249,6 +277,8 @@ function ColumnPropertyView<T extends MultiBaseComp<TableChildrenType>>(props: {
249277
comp: T;
250278
columnLabel: string;
251279
}) {
280+
const [viewMode, setViewMode] = useState('normal');
281+
const [summaryRow, setSummaryRow] = useState(0);
252282
const { comp } = props;
253283
const selection = getSelectedRowKeys(comp.children.selection)[0] ?? "0";
254284
const [columnFilterType, setColumnFilterType] = useState<ColumnFilterOptionValueType>("all");
@@ -261,6 +291,7 @@ function ColumnPropertyView<T extends MultiBaseComp<TableChildrenType>>(props: {
261291
() => columns.filter((c) => columnFilterType === "all" || !c.children.hide.getView()),
262292
[columnFilterType, columns]
263293
);
294+
const summaryRows = parseInt(comp.children.summaryRows.getView());
264295

265296
const columnOptionToolbar = (
266297
<InsertDiv>
@@ -365,7 +396,21 @@ function ColumnPropertyView<T extends MultiBaseComp<TableChildrenType>>(props: {
365396
}}
366397
content={(column, index) => (
367398
<>
368-
{column.propertyView(selection)}
399+
<Segmented
400+
block
401+
options={columnViewOptions}
402+
value={viewMode}
403+
onChange={(k) => setViewMode(k as ViewOptionType)}
404+
/>
405+
{viewMode === 'summary' && (
406+
<Segmented
407+
block
408+
options={summaryRowOptions.slice(0, summaryRows)}
409+
value={summaryRow}
410+
onChange={(k) => setSummaryRow(k)}
411+
/>
412+
)}
413+
{column.propertyView(selection, viewMode, summaryRow)}
369414
{column.getView().isCustom && (
370415
<RedButton
371416
onClick={() => {
@@ -418,6 +463,7 @@ function columnPropertyView<T extends MultiBaseComp<TableChildrenType>>(comp: T)
418463
export function compTablePropertyView<T extends MultiBaseComp<TableChildrenType> & { editorModeStatus: string }>(comp: T) {
419464
const editorModeStatus = comp.editorModeStatus;
420465
const dataLabel = trans("data");
466+
421467
return (
422468
<>
423469
{["logic", "both"].includes(editorModeStatus) && (
@@ -448,6 +494,16 @@ export function compTablePropertyView<T extends MultiBaseComp<TableChildrenType>
448494
{loadingPropertyView(comp.children)}
449495
</Section>
450496

497+
<Section name={"Summary"}>
498+
{comp.children.showSummary.propertyView({
499+
label: trans("table.showSummary")
500+
})}
501+
{comp.children.summaryRows.propertyView({
502+
label: trans("table.totalSummaryRows"),
503+
radioButton: true,
504+
})}
505+
</Section>
506+
451507
<Section name={trans("prop.toolbar")}>
452508
{comp.children.toolbar.getPropertyView()}
453509
</Section>
@@ -494,6 +550,9 @@ export function compTablePropertyView<T extends MultiBaseComp<TableChildrenType>
494550
<>
495551
<Section name={sectionNames.advanced}>
496552
{comp.children.expansion.getPropertyView()}
553+
{comp.children.inlineAddNewRow.propertyView({
554+
label: trans("table.inlineAddNewRow")
555+
})}
497556
{comp.children.showDataLoadSpinner.propertyView({
498557
label: trans("table.showDataLoadSpinner"),
499558
})}
@@ -503,6 +562,10 @@ export function compTablePropertyView<T extends MultiBaseComp<TableChildrenType>
503562
label: trans("table.dynamicColumnConfig"),
504563
tooltip: trans("table.dynamicColumnConfigDesc"),
505564
})}
565+
{comp.children.editModeClicks.propertyView({
566+
label: trans("table.editMode"),
567+
radioButton: true,
568+
})}
506569
{comp.children.searchText.propertyView({
507570
label: trans("table.searchText"),
508571
tooltip: trans("table.searchTextTooltip"),
@@ -537,6 +600,9 @@ export function compTablePropertyView<T extends MultiBaseComp<TableChildrenType>
537600
<Section name={"Column Style"}>
538601
{comp.children.columnsStyle.getPropertyView()}
539602
</Section>
603+
<Section name={"Summary Row Style"}>
604+
{comp.children.summaryRowStyle.getPropertyView()}
605+
</Section>
540606
</>
541607
)}
542608
</>
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { ThemeDetail } from "api/commonSettingApi";
2+
import { ColumnComp } from "comps/comps/tableComp/column/tableColumnComp";
3+
import { TableColumnLinkStyleType, TableColumnStyleType, TableSummaryRowStyleType } from "comps/controls/styleControlConstants";
4+
import styled from "styled-components";
5+
import { defaultTheme } from "@lowcoder-ee/constants/themeConstants";
6+
import Table from "antd/es/table";
7+
import { ReactNode } from "react";
8+
import Tooltip from "antd/es/tooltip";
9+
10+
const TableSummaryRow = styled(Table.Summary.Row)<{
11+
$istoolbarPositionBelow: boolean;
12+
}>`
13+
td:last-child {
14+
border-right: unset !important;
15+
}
16+
17+
${props => !props.$istoolbarPositionBelow && `
18+
&:last-child td {
19+
border-bottom: none !important;
20+
}
21+
`}
22+
23+
`;
24+
25+
const TableSummarCell = styled(Table.Summary.Cell)<{
26+
$style: TableSummaryRowStyleType;
27+
$defaultThemeDetail: ThemeDetail;
28+
$linkStyle?: TableColumnLinkStyleType;
29+
$tableSize?: string;
30+
$autoHeight?: boolean;
31+
}>`
32+
background: ${(props) => props.$style.background} !important;
33+
border-color: ${(props) => props.$style.border} !important;
34+
// border-width: ${(props) => props.$style.borderWidth} !important;
35+
// border-style: ${(props) => props.$style.borderStyle} !important;
36+
border-radius: ${(props) => props.$style.radius};
37+
padding: 0 !important;
38+
39+
> div {
40+
margin: ${(props) => props.$style.margin};
41+
color: ${(props) => props.$style.text};
42+
font-weight: ${(props) => props.$style.textWeight};
43+
font-family: ${(props) => props.$style.fontFamily};
44+
overflow: hidden;
45+
${(props) => props.$tableSize === 'small' && `
46+
padding: 1px 8px;
47+
font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '14px !important' : props.$style.textSize + ' !important'};
48+
font-style:${props.$style.fontStyle} !important;
49+
min-height: 14px;
50+
line-height: 20px;
51+
${!props.$autoHeight && `
52+
overflow-y: auto;
53+
max-height: 28px;
54+
`};
55+
`};
56+
${(props) => props.$tableSize === 'middle' && `
57+
padding: 8px 8px;
58+
font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '16px !important' : props.$style.textSize + ' !important'};
59+
font-style:${props.$style.fontStyle} !important;
60+
min-height: 24px;
61+
line-height: 24px;
62+
${!props.$autoHeight && `
63+
overflow-y: auto;
64+
max-height: 48px;
65+
`};
66+
`};
67+
${(props) => props.$tableSize === 'large' && `
68+
padding: 16px 16px;
69+
font-size: ${props.$defaultThemeDetail.textSize == props.$style.textSize ? '18px !important' : props.$style.textSize + ' !important'};
70+
font-style:${props.$style.fontStyle} !important;
71+
min-height: 48px;
72+
${!props.$autoHeight && `
73+
overflow-y: auto;
74+
max-height: 96px;
75+
`};
76+
`};
77+
78+
> .ant-badge > .ant-badge-status-text,
79+
> div > .markdown-body {
80+
color: ${(props) => props.$style.text};
81+
}
82+
83+
> div > svg g {
84+
stroke: ${(props) => props.$style.text};
85+
}
86+
87+
> a,
88+
> div a {
89+
color: ${(props) => props.$linkStyle?.text};
90+
91+
&:hover {
92+
color: ${(props) => props.$linkStyle?.hoverText};
93+
}
94+
95+
&:active {
96+
color: ${(props) => props.$linkStyle?.activeText}};
97+
}
98+
}
99+
}
100+
`;
101+
102+
const CellWrapper = ({
103+
children,
104+
tooltipTitle,
105+
}: {
106+
children: ReactNode,
107+
tooltipTitle?: string,
108+
}) => {
109+
if (tooltipTitle) {
110+
return (
111+
<Tooltip title={tooltipTitle} placement="topLeft">
112+
{children}
113+
</Tooltip>
114+
)
115+
}
116+
return (
117+
<>{children}</>
118+
)
119+
};
120+
121+
function TableSummaryCellView(props: {
122+
index: number;
123+
key: string;
124+
children: any;
125+
align?: any;
126+
rowStyle: TableSummaryRowStyleType;
127+
columnStyle: TableColumnStyleType;
128+
linkStyle: TableColumnLinkStyleType;
129+
tableSize?: string;
130+
autoHeight?: boolean;
131+
cellColor: string;
132+
cellTooltip: string;
133+
}) {
134+
const {
135+
children,
136+
rowStyle,
137+
columnStyle,
138+
tableSize,
139+
autoHeight,
140+
cellColor,
141+
cellTooltip,
142+
...restProps
143+
} = props;
144+
145+
const style = {
146+
background: cellColor || columnStyle.background || rowStyle.background,
147+
margin: columnStyle.margin || rowStyle.margin,
148+
text: columnStyle.text || rowStyle.text,
149+
border: columnStyle.border || rowStyle.border,
150+
borderWidth: rowStyle.borderWidth,
151+
borderStyle: rowStyle.borderStyle,
152+
radius: columnStyle.radius || rowStyle.radius,
153+
textSize: columnStyle.textSize || rowStyle.textSize,
154+
textWeight: rowStyle.textWeight || columnStyle.textWeight,
155+
fontFamily: rowStyle.fontFamily || columnStyle.fontFamily,
156+
fontStyle: rowStyle.fontStyle || columnStyle.fontStyle,
157+
}
158+
159+
return (
160+
<TableSummarCell
161+
{...restProps}
162+
$style={style}
163+
$defaultThemeDetail={defaultTheme}
164+
$tableSize={tableSize}
165+
$autoHeight={autoHeight}
166+
>
167+
<CellWrapper tooltipTitle={cellTooltip}>
168+
<div>{children}</div>
169+
</CellWrapper>
170+
</TableSummarCell>
171+
);
172+
}
173+
174+
export function TableSummary(props: {
175+
tableSize: string;
176+
expandableRows: boolean;
177+
summaryRows: number;
178+
columns: ColumnComp[];
179+
summaryRowStyle: TableSummaryRowStyleType;
180+
istoolbarPositionBelow: boolean;
181+
}) {
182+
const {
183+
columns,
184+
summaryRows,
185+
summaryRowStyle,
186+
tableSize,
187+
expandableRows,
188+
istoolbarPositionBelow,
189+
} = props;
190+
let visibleColumns = columns.filter(col => !col.getView().hide);
191+
if (expandableRows) {
192+
visibleColumns.unshift(new ColumnComp({}));
193+
}
194+
195+
if (!visibleColumns.length) return <></>;
196+
197+
return (
198+
<Table.Summary>
199+
{Array.from(Array(summaryRows)).map((_, rowIndex) => (
200+
<TableSummaryRow key={rowIndex} $istoolbarPositionBelow={istoolbarPositionBelow}>
201+
{visibleColumns.map((column, index) => {
202+
const summaryColumn = column.children.summaryColumns.getView()[rowIndex].getView();
203+
return (
204+
<TableSummaryCellView
205+
index={index}
206+
key={`summary-${rowIndex}-${column.getView().dataIndex}-${index}`}
207+
tableSize={tableSize}
208+
rowStyle={summaryRowStyle}
209+
align={summaryColumn.align}
210+
cellColor={summaryColumn.cellColor}
211+
cellTooltip={summaryColumn.cellTooltip}
212+
columnStyle={{
213+
background: summaryColumn.background,
214+
margin: summaryColumn.margin,
215+
text: summaryColumn.text,
216+
border: summaryColumn.border,
217+
radius: summaryColumn.radius,
218+
textSize: summaryColumn.textSize,
219+
textWeight: summaryColumn.textWeight,
220+
fontStyle:summaryColumn.fontStyle,
221+
fontFamily: summaryColumn.fontFamily,
222+
}}
223+
linkStyle={{
224+
text: summaryColumn.linkColor,
225+
hoverText: summaryColumn.linkHoverColor,
226+
activeText: summaryColumn.linkActiveColor,
227+
}}
228+
>
229+
{summaryColumn.render({}, '').getView().view({})}
230+
</TableSummaryCellView>
231+
)
232+
})}
233+
</TableSummaryRow>
234+
))}
235+
</Table.Summary>
236+
);
237+
}

‎client/packages/lowcoder/src/comps/comps/tableComp/tableTypes.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { dropdownControl } from "comps/controls/dropdownControl";
1414
import { eventHandlerControl } from "comps/controls/eventHandlerControl";
1515
import { styleControl } from "comps/controls/styleControl";
16-
import { TableColumnStyle, TableRowStyle, TableStyle, TableToolbarStyle, TableHeaderStyle } from "comps/controls/styleControlConstants";
16+
import { TableColumnStyle, TableRowStyle, TableStyle, TableToolbarStyle, TableHeaderStyle, TableSummaryRowStyle } from "comps/controls/styleControlConstants";
1717
import {
1818
MultiCompBuilder,
1919
stateComp,
@@ -31,12 +31,38 @@ import {
3131
RecordConstructorToView,
3232
} from "lowcoder-core";
3333
import { controlItem } from "lowcoder-design";
34-
import { JSONObject } from "util/jsonTypes";
34+
import { JSONArray, JSONObject } from "util/jsonTypes";
3535
import { ExpansionControl } from "./expansionControl";
3636
import { PaginationControl } from "./paginationControl";
3737
import { SelectionControl } from "./selectionControl";
3838
import { AutoHeightControl } from "comps/controls/autoHeightControl";
3939

40+
const editModeClickOptions = [
41+
{
42+
label: trans("table.singleClick"),
43+
value: "single",
44+
},
45+
{
46+
label: trans("table.doubleClick"),
47+
value: "double",
48+
},
49+
] as const;
50+
51+
const summarRowsOptions = [
52+
{
53+
label: "1",
54+
value: "1",
55+
},
56+
{
57+
label: "2",
58+
value: "2",
59+
},
60+
{
61+
label: "3",
62+
value: "3",
63+
},
64+
] as const;
65+
4066
const sizeOptions = [
4167
{
4268
label: trans("table.small"),
@@ -200,15 +226,19 @@ const tableChildrenMap = {
200226
showVerticalScrollbar: BoolControl,
201227
showHorizontalScrollbar: BoolControl,
202228
data: withIsLoadingMethod(JSONObjectArrayControl),
229+
newData: stateComp<JSONArray>([]),
203230
showDataLoadSpinner: withDefault(BoolPureControl, true),
204231
columns: ColumnListComp,
205232
size: dropdownControl(sizeOptions, "middle"),
206233
selection: SelectionControl,
207234
pagination: PaginationControl,
208235
sort: valueComp<Array<SortValue>>([]),
209236
toolbar: TableToolbarComp,
237+
showSummary: BoolControl,
238+
summaryRows: dropdownControl(summarRowsOptions, "1"),
210239
style: styleControl(TableStyle, 'style'),
211240
rowStyle: styleControl(TableRowStyle, 'rowStyle'),
241+
summaryRowStyle: styleControl(TableSummaryRowStyle, 'summaryRowStyle'),
212242
toolbarStyle: styleControl(TableToolbarStyle, 'toolbarStyle'),
213243
headerStyle: styleControl(TableHeaderStyle, 'headerStyle'),
214244
searchText: StringControl,
@@ -228,6 +258,8 @@ const tableChildrenMap = {
228258
dynamicColumnConfig: ArrayStringControl,
229259
expansion: ExpansionControl,
230260
selectedCell: stateComp<JSONObject>({}),
261+
inlineAddNewRow: BoolControl,
262+
editModeClicks: dropdownControl(editModeClickOptions, "single"),
231263
};
232264

233265
export const TableInitComp = (function () {

‎client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ColumnListComp, tableDataRowExample } from "./column/tableColumnListCom
2020
import { TableColumnLinkStyleType, TableColumnStyleType } from "comps/controls/styleControlConstants";
2121
import Tooltip from "antd/es/tooltip";
2222
import InfoCircleOutlined from "@ant-design/icons/InfoCircleOutlined";
23+
import { EMPTY_ROW_KEY } from "./tableCompView";
2324

2425
export const COLUMN_CHILDREN_KEY = "children";
2526
export const OB_ROW_ORI_INDEX = "__ob_origin_index";
@@ -294,6 +295,7 @@ export function columnsToAntdFormat(
294295
dynamicColumn: boolean,
295296
dynamicColumnConfig: Array<string>,
296297
columnsAggrData: ColumnsAggrData,
298+
editMode: string,
297299
onTableEvent: (eventName: any) => void,
298300
): Array<CustomColumnType<RecordType>> {
299301
const customColumns = columns.filter(col => col.isCustom).map(col => col.dataIndex);
@@ -381,7 +383,7 @@ export function columnsToAntdFormat(
381383
)
382384
.getView()
383385
.view({
384-
editable: column.editable,
386+
editable: record[OB_ROW_ORI_INDEX].startsWith(EMPTY_ROW_KEY) || column.editable,
385387
size,
386388
candidateTags: tags,
387389
candidateStatus: status,
@@ -391,6 +393,7 @@ export function columnsToAntdFormat(
391393
currentRow: row,
392394
currentIndex: index,
393395
}),
396+
editMode,
394397
onTableEvent,
395398
});
396399
},

‎client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1545,7 +1545,6 @@ export const TableColumnStyle = [
15451545
getStaticBackground("#00000000"),
15461546
getStaticBorder(),
15471547
MARGIN,
1548-
15491548
RADIUS,
15501549
TEXT,
15511550
TEXT_SIZE,
@@ -1556,6 +1555,18 @@ export const TableColumnStyle = [
15561555

15571556
export const TableColumnLinkStyle = [...LinkTextStyle] as const;
15581557

1558+
export const TableSummaryRowStyle = [
1559+
BORDER_WIDTH,
1560+
BORDER_STYLE,
1561+
...BG_STATIC_BORDER_RADIUS,
1562+
MARGIN,
1563+
TEXT,
1564+
TEXT_SIZE,
1565+
TEXT_WEIGHT,
1566+
FONT_FAMILY,
1567+
FONT_STYLE,
1568+
] as const;
1569+
15591570
export const FileStyle = [
15601571
// ...getStaticBgBorderRadiusByBg(SURFACE_COLOR),
15611572
getStaticBackground(SURFACE_COLOR),
@@ -2020,6 +2031,7 @@ export type TableColumnStyleType = StyleConfigType<typeof TableColumnStyle>;
20202031
export type TableColumnLinkStyleType = StyleConfigType<
20212032
typeof TableColumnLinkStyle
20222033
>;
2034+
export type TableSummaryRowStyleType = StyleConfigType<typeof TableSummaryRowStyle>;
20232035
export type FileStyleType = StyleConfigType<typeof FileStyle>;
20242036
export type FileViewerStyleType = StyleConfigType<typeof FileViewerStyle>;
20252037
export type IframeStyleType = StyleConfigType<typeof IframeStyle>;

‎client/packages/lowcoder/src/constants/themeConstants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ const table = {
7171
},
7272
columnsStyle: {
7373
radius: '0px'
74+
},
75+
summaryRowStyle: {
76+
radius: '0px'
7477
}
7578
}
7679

‎client/packages/lowcoder/src/i18n/locales/en.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1998,7 +1998,13 @@ export const en = {
19981998
"iconNull": "Icon When Null",
19991999
"allColumn": "All",
20002000
"visibleColumn": "Visible",
2001-
"emptyColumns": "No Columns Are Currently Visible"
2001+
"emptyColumns": "No Columns Are Currently Visible",
2002+
"showSummary": "Show Summary Row",
2003+
"totalSummaryRows": "Total Rows",
2004+
"inlineAddNewRow": "Inline Add New Row",
2005+
"editMode": "Edit Mode",
2006+
"singleClick": "Single Click",
2007+
"doubleClick": "Double Click",
20022008
},
20032009

20042010

0 commit comments

Comments
 (0)
Please sign in to comment.