Skip to content

Commit 2addb44

Browse files
committed
[Feat]: #2089 add password column type
1 parent 3eca6dc commit 2addb44

File tree

6 files changed

+292
-2
lines changed

6 files changed

+292
-2
lines changed

client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { ColumnNumberComp } from "./columnTypeComps/ColumnNumberComp";
2222

2323
import { ColumnAvatarsComp } from "./columnTypeComps/columnAvatarsComp";
2424
import { ColumnDropdownComp } from "./columnTypeComps/columnDropdownComp";
25+
import { ColumnPasswordComp } from "./columnTypeComps/columnPasswordComp";
2526

2627
const actionOptions = [
2728
{
@@ -101,6 +102,10 @@ const actionOptions = [
101102
label: trans("table.progress"),
102103
value: "progress",
103104
},
105+
{
106+
label: "Password",
107+
value: "password",
108+
},
104109
] as const;
105110

106111
export const ColumnTypeCompMap = {
@@ -123,6 +128,7 @@ export const ColumnTypeCompMap = {
123128
progress: ProgressComp,
124129
date: DateComp,
125130
time: TimeComp,
131+
password: ColumnPasswordComp,
126132
};
127133

128134
type ColumnTypeMapType = typeof ColumnTypeCompMap;
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import React, { useCallback, useMemo, useState } from "react";
2+
import styled from "styled-components";
3+
import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons";
4+
import { default as Input } from "antd/es/input";
5+
import { StringOrNumberControl } from "comps/controls/codeControl";
6+
import { ColumnTypeCompBuilder, ColumnTypeViewFn } from "../columnTypeCompBuilder";
7+
import { ColumnValueTooltip } from "../simpleColumnTypeComps";
8+
9+
const Wrapper = styled.div`
10+
display: inline-flex;
11+
align-items: center;
12+
min-width: 0;
13+
gap: 6px;
14+
`;
15+
16+
const ValueText = styled.span`
17+
min-width: 0;
18+
overflow: hidden;
19+
text-overflow: ellipsis;
20+
white-space: nowrap;
21+
font-variant-ligatures: none;
22+
`;
23+
24+
const ToggleButton = styled.button`
25+
all: unset;
26+
display: inline-flex;
27+
align-items: center;
28+
cursor: pointer;
29+
opacity: 0.6;
30+
31+
&:hover {
32+
opacity: 1;
33+
}
34+
35+
svg {
36+
font-size: 14px;
37+
}
38+
`;
39+
40+
const childrenMap = {
41+
text: StringOrNumberControl,
42+
};
43+
44+
function normalizeToString(value: unknown) {
45+
if (value === null || value === undefined) return "";
46+
return typeof value === "string" ? value : String(value);
47+
}
48+
49+
function maskPassword(raw: string, maskChar = "•", maxMaskLen = 12) {
50+
if (!raw) return "";
51+
const len = raw.length;
52+
const maskLen = Math.min(len, maxMaskLen);
53+
const masked = maskChar.repeat(maskLen);
54+
return len > maxMaskLen ? `${masked}…` : masked;
55+
}
56+
57+
const getBaseValue: ColumnTypeViewFn<typeof childrenMap, string | number, string> = (props) =>
58+
normalizeToString(props.text);
59+
60+
const PasswordCell = React.memo(
61+
({
62+
value,
63+
cellIndex,
64+
}: {
65+
value: string;
66+
cellIndex?: string;
67+
}) => {
68+
const [visible, setVisible] = useState(false);
69+
70+
const masked = useMemo(() => maskPassword(value), [value]);
71+
72+
const onToggle = useCallback((e: React.MouseEvent) => {
73+
e.preventDefault();
74+
e.stopPropagation();
75+
setVisible((v) => !v);
76+
}, []);
77+
78+
React.useEffect(() => {
79+
setVisible(false);
80+
}, [cellIndex, value]);
81+
82+
if (!value) {
83+
return <span />;
84+
}
85+
86+
return (
87+
<Wrapper>
88+
<ValueText>{visible ? value : masked}</ValueText>
89+
<ToggleButton onClick={onToggle} aria-label={visible ? "Hide password" : "Show password"}>
90+
{visible ? <EyeInvisibleOutlined /> : <EyeOutlined />}
91+
</ToggleButton>
92+
</Wrapper>
93+
);
94+
}
95+
);
96+
97+
PasswordCell.displayName = "PasswordCell";
98+
99+
const PasswordEditView = React.memo(
100+
({
101+
value,
102+
onChange,
103+
onChangeEnd,
104+
}: {
105+
value: string;
106+
onChange: (value: string) => void;
107+
onChangeEnd: () => void;
108+
}) => {
109+
const handleChange = useCallback(
110+
(e: React.ChangeEvent<HTMLInputElement>) => {
111+
onChange(e.target.value);
112+
},
113+
[onChange]
114+
);
115+
116+
return (
117+
<Input.Password
118+
defaultValue={value}
119+
autoFocus
120+
variant="borderless"
121+
onChange={handleChange}
122+
onBlur={onChangeEnd}
123+
onPressEnter={onChangeEnd}
124+
visibilityToggle={true}
125+
/>
126+
);
127+
}
128+
);
129+
130+
PasswordEditView.displayName = "PasswordEditView";
131+
132+
export const ColumnPasswordComp = new ColumnTypeCompBuilder(
133+
childrenMap,
134+
(props, dispatch) => {
135+
const value = props.changeValue ?? getBaseValue(props, dispatch);
136+
return <PasswordCell value={normalizeToString(value)} cellIndex={(props as any).cellIndex} />;
137+
},
138+
(nodeValue) => maskPassword(normalizeToString(nodeValue.text.value)),
139+
getBaseValue
140+
)
141+
.setEditViewFn((props) => {
142+
return (
143+
<PasswordEditView
144+
value={normalizeToString(props.value)}
145+
onChange={(v) => props.onChange(v)}
146+
onChangeEnd={props.onChangeEnd}
147+
/>
148+
);
149+
})
150+
.setPropertyViewFn((children) => (
151+
<>
152+
{children.text.propertyView({
153+
label: "Value",
154+
tooltip: ColumnValueTooltip,
155+
})}
156+
</>
157+
))
158+
.build();
159+
160+

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ export function columnsToAntdFormat(
451451
}),
452452
editMode,
453453
onTableEvent,
454-
cellIndex: `${column.dataIndex}-${index}`,
454+
cellIndex: `${column.dataIndex}-${record?.[OB_ROW_ORI_INDEX] ?? index}`,
455455
});
456456
},
457457
...(column.sortable

client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComp.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ColumnNumberComp } from "./columnTypeComps/ColumnNumberComp";
2121

2222
import { ColumnAvatarsComp } from "./columnTypeComps/columnAvatarsComp";
2323
import { ColumnDropdownComp } from "./columnTypeComps/columnDropdownComp";
24+
import { ColumnPasswordComp } from "./columnTypeComps/columnPasswordComp";
2425

2526
export type CellProps = {
2627
tableSize?: string;
@@ -110,6 +111,10 @@ const actionOptions = [
110111
label: trans("table.progress"),
111112
value: "progress",
112113
},
114+
{
115+
label: "Password",
116+
value: "password",
117+
},
113118
] as const;
114119

115120
export const ColumnTypeCompMap = {
@@ -132,6 +137,7 @@ export const ColumnTypeCompMap = {
132137
progress: ProgressComp,
133138
date: DateComp,
134139
time: TimeComp,
140+
password: ColumnPasswordComp,
135141
};
136142

137143
type ColumnTypeMapType = typeof ColumnTypeCompMap;
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { useCallback, useMemo, useState } from "react";
2+
import styled from "styled-components";
3+
import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons";
4+
import { StringOrNumberControl } from "comps/controls/codeControl";
5+
import { ColumnTypeCompBuilder, ColumnTypeViewFn } from "../columnTypeCompBuilder";
6+
import { ColumnValueTooltip } from "../simpleColumnTypeComps";
7+
8+
const Wrapper = styled.div`
9+
display: inline-flex;
10+
align-items: center;
11+
min-width: 0;
12+
gap: 6px;
13+
`;
14+
15+
const ValueText = styled.span`
16+
min-width: 0;
17+
overflow: hidden;
18+
text-overflow: ellipsis;
19+
white-space: nowrap;
20+
font-variant-ligatures: none;
21+
`;
22+
23+
const ToggleButton = styled.button`
24+
all: unset;
25+
display: inline-flex;
26+
align-items: center;
27+
cursor: pointer;
28+
opacity: 0.6;
29+
30+
&:hover {
31+
opacity: 1;
32+
}
33+
34+
svg {
35+
font-size: 14px;
36+
}
37+
`;
38+
39+
const childrenMap = {
40+
text: StringOrNumberControl,
41+
};
42+
43+
function normalizeToString(value: unknown) {
44+
if (value === null || value === undefined) return "";
45+
return typeof value === "string" ? value : String(value);
46+
}
47+
48+
function maskPassword(raw: string, maskChar = "•", maxMaskLen = 12) {
49+
if (!raw) return "";
50+
const len = raw.length;
51+
const maskLen = Math.min(len, maxMaskLen);
52+
const masked = maskChar.repeat(maskLen);
53+
return len > maxMaskLen ? `${masked}…` : masked;
54+
}
55+
56+
const getBaseValue: ColumnTypeViewFn<typeof childrenMap, string | number, string> = (props) =>
57+
normalizeToString(props.text);
58+
59+
const PasswordCell = React.memo(
60+
({
61+
value,
62+
cellIndex,
63+
}: {
64+
value: string;
65+
cellIndex?: string;
66+
}) => {
67+
const [visible, setVisible] = useState(false);
68+
69+
const masked = useMemo(() => maskPassword(value), [value]);
70+
71+
const onToggle = useCallback((e: React.MouseEvent) => {
72+
e.preventDefault();
73+
e.stopPropagation();
74+
setVisible((v) => !v);
75+
}, []);
76+
77+
78+
React.useEffect(() => {
79+
setVisible(false);
80+
}, [cellIndex, value]);
81+
82+
if (!value) {
83+
return <span />;
84+
}
85+
86+
return (
87+
<Wrapper>
88+
<ValueText>{visible ? value : masked}</ValueText>
89+
<ToggleButton onClick={onToggle} aria-label={visible ? "Hide password" : "Show password"}>
90+
{visible ? <EyeInvisibleOutlined /> : <EyeOutlined />}
91+
</ToggleButton>
92+
</Wrapper>
93+
);
94+
}
95+
);
96+
97+
PasswordCell.displayName = "PasswordCell";
98+
99+
export const ColumnPasswordComp = new ColumnTypeCompBuilder(
100+
childrenMap,
101+
(props, dispatch) => {
102+
const value = getBaseValue(props, dispatch);
103+
return <PasswordCell value={value} cellIndex={(props as any).cellIndex} />;
104+
},
105+
(nodeValue) => maskPassword(normalizeToString(nodeValue.text.value)),
106+
getBaseValue
107+
)
108+
.setPropertyViewFn((children) => (
109+
<>
110+
{children.text.propertyView({
111+
label: "Value",
112+
tooltip: ColumnValueTooltip,
113+
})}
114+
</>
115+
))
116+
.build();
117+
118+

client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ function buildRenderFn(
490490
currentIndex: index,
491491
}),
492492
onTableEvent,
493-
cellIndex: `${column.dataIndex}-${index}`,
493+
cellIndex: `${column.dataIndex}-${record?.[OB_ROW_ORI_INDEX] ?? index}`,
494494
});
495495
};
496496
}

0 commit comments

Comments
 (0)