diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx index ff3df44c4..e2e82f58d 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx @@ -22,6 +22,7 @@ import { ColumnNumberComp } from "./columnTypeComps/ColumnNumberComp"; import { ColumnAvatarsComp } from "./columnTypeComps/columnAvatarsComp"; import { ColumnDropdownComp } from "./columnTypeComps/columnDropdownComp"; +import { ColumnPasswordComp } from "./columnTypeComps/columnPasswordComp"; const actionOptions = [ { @@ -101,6 +102,10 @@ const actionOptions = [ label: trans("table.progress"), value: "progress", }, + { + label: "Password", + value: "password", + }, ] as const; export const ColumnTypeCompMap = { @@ -123,6 +128,7 @@ export const ColumnTypeCompMap = { progress: ProgressComp, date: DateComp, time: TimeComp, + password: ColumnPasswordComp, }; type ColumnTypeMapType = typeof ColumnTypeCompMap; diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnPasswordComp.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnPasswordComp.tsx new file mode 100644 index 000000000..5c0d62ff4 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/columnPasswordComp.tsx @@ -0,0 +1,160 @@ +import React, { useCallback, useMemo, useState } from "react"; +import styled from "styled-components"; +import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons"; +import { default as Input } from "antd/es/input"; +import { StringOrNumberControl } from "comps/controls/codeControl"; +import { ColumnTypeCompBuilder, ColumnTypeViewFn } from "../columnTypeCompBuilder"; +import { ColumnValueTooltip } from "../simpleColumnTypeComps"; + +const Wrapper = styled.div` + display: inline-flex; + align-items: center; + min-width: 0; + gap: 6px; +`; + +const ValueText = styled.span` + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-variant-ligatures: none; +`; + +const ToggleButton = styled.button` + all: unset; + display: inline-flex; + align-items: center; + cursor: pointer; + opacity: 0.6; + + &:hover { + opacity: 1; + } + + svg { + font-size: 14px; + } +`; + +const childrenMap = { + text: StringOrNumberControl, +}; + +function normalizeToString(value: unknown) { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +function maskPassword(raw: string, maskChar = "•", maxMaskLen = 12) { + if (!raw) return ""; + const len = raw.length; + const maskLen = Math.min(len, maxMaskLen); + const masked = maskChar.repeat(maskLen); + return len > maxMaskLen ? `${masked}…` : masked; +} + +const getBaseValue: ColumnTypeViewFn = (props) => + normalizeToString(props.text); + +const PasswordCell = React.memo( + ({ + value, + cellIndex, + }: { + value: string; + cellIndex?: string; + }) => { + const [visible, setVisible] = useState(false); + + const masked = useMemo(() => maskPassword(value), [value]); + + const onToggle = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setVisible((v) => !v); + }, []); + + React.useEffect(() => { + setVisible(false); + }, [cellIndex, value]); + + if (!value) { + return ; + } + + return ( + + {visible ? value : masked} + + {visible ? : } + + + ); + } +); + +PasswordCell.displayName = "PasswordCell"; + +const PasswordEditView = React.memo( + ({ + value, + onChange, + onChangeEnd, + }: { + value: string; + onChange: (value: string) => void; + onChangeEnd: () => void; + }) => { + const handleChange = useCallback( + (e: React.ChangeEvent) => { + onChange(e.target.value); + }, + [onChange] + ); + + return ( + + ); + } +); + +PasswordEditView.displayName = "PasswordEditView"; + +export const ColumnPasswordComp = new ColumnTypeCompBuilder( + childrenMap, + (props, dispatch) => { + const value = props.changeValue ?? getBaseValue(props, dispatch); + return ; + }, + (nodeValue) => maskPassword(normalizeToString(nodeValue.text.value)), + getBaseValue +) + .setEditViewFn((props) => { + return ( + props.onChange(v)} + onChangeEnd={props.onChangeEnd} + /> + ); + }) + .setPropertyViewFn((children) => ( + <> + {children.text.propertyView({ + label: "Value", + tooltip: ColumnValueTooltip, + })} + + )) + .build(); + + diff --git a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx index edb26ca61..69948171f 100644 --- a/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx @@ -451,7 +451,7 @@ export function columnsToAntdFormat( }), editMode, onTableEvent, - cellIndex: `${column.dataIndex}-${index}`, + cellIndex: `${column.dataIndex}-${record?.[OB_ROW_ORI_INDEX] ?? index}`, }); }, ...(column.sortable diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComp.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComp.tsx index 842057fbb..5f585df6e 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComp.tsx @@ -21,6 +21,7 @@ import { ColumnNumberComp } from "./columnTypeComps/ColumnNumberComp"; import { ColumnAvatarsComp } from "./columnTypeComps/columnAvatarsComp"; import { ColumnDropdownComp } from "./columnTypeComps/columnDropdownComp"; +import { ColumnPasswordComp } from "./columnTypeComps/columnPasswordComp"; export type CellProps = { tableSize?: string; @@ -110,6 +111,10 @@ const actionOptions = [ label: trans("table.progress"), value: "progress", }, + { + label: "Password", + value: "password", + }, ] as const; export const ColumnTypeCompMap = { @@ -132,6 +137,7 @@ export const ColumnTypeCompMap = { progress: ProgressComp, date: DateComp, time: TimeComp, + password: ColumnPasswordComp, }; type ColumnTypeMapType = typeof ColumnTypeCompMap; diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComps/columnPasswordComp.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComps/columnPasswordComp.tsx new file mode 100644 index 000000000..41739d0f5 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/column/columnTypeComps/columnPasswordComp.tsx @@ -0,0 +1,118 @@ +import React, { useCallback, useMemo, useState } from "react"; +import styled from "styled-components"; +import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons"; +import { StringOrNumberControl } from "comps/controls/codeControl"; +import { ColumnTypeCompBuilder, ColumnTypeViewFn } from "../columnTypeCompBuilder"; +import { ColumnValueTooltip } from "../simpleColumnTypeComps"; + +const Wrapper = styled.div` + display: inline-flex; + align-items: center; + min-width: 0; + gap: 6px; +`; + +const ValueText = styled.span` + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-variant-ligatures: none; +`; + +const ToggleButton = styled.button` + all: unset; + display: inline-flex; + align-items: center; + cursor: pointer; + opacity: 0.6; + + &:hover { + opacity: 1; + } + + svg { + font-size: 14px; + } +`; + +const childrenMap = { + text: StringOrNumberControl, +}; + +function normalizeToString(value: unknown) { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +function maskPassword(raw: string, maskChar = "•", maxMaskLen = 12) { + if (!raw) return ""; + const len = raw.length; + const maskLen = Math.min(len, maxMaskLen); + const masked = maskChar.repeat(maskLen); + return len > maxMaskLen ? `${masked}…` : masked; +} + +const getBaseValue: ColumnTypeViewFn = (props) => + normalizeToString(props.text); + +const PasswordCell = React.memo( + ({ + value, + cellIndex, + }: { + value: string; + cellIndex?: string; + }) => { + const [visible, setVisible] = useState(false); + + const masked = useMemo(() => maskPassword(value), [value]); + + const onToggle = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setVisible((v) => !v); + }, []); + + + React.useEffect(() => { + setVisible(false); + }, [cellIndex, value]); + + if (!value) { + return ; + } + + return ( + + {visible ? value : masked} + + {visible ? : } + + + ); + } +); + +PasswordCell.displayName = "PasswordCell"; + +export const ColumnPasswordComp = new ColumnTypeCompBuilder( + childrenMap, + (props, dispatch) => { + const value = getBaseValue(props, dispatch); + return ; + }, + (nodeValue) => maskPassword(normalizeToString(nodeValue.text.value)), + getBaseValue +) + .setPropertyViewFn((children) => ( + <> + {children.text.propertyView({ + label: "Value", + tooltip: ColumnValueTooltip, + })} + + )) + .build(); + + diff --git a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx index 69f18ae83..7e24bd33b 100644 --- a/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx +++ b/client/packages/lowcoder/src/comps/comps/tableLiteComp/tableUtils.tsx @@ -490,7 +490,7 @@ function buildRenderFn( currentIndex: index, }), onTableEvent, - cellIndex: `${column.dataIndex}-${index}`, + cellIndex: `${column.dataIndex}-${record?.[OB_ROW_ORI_INDEX] ?? index}`, }); }; }