Skip to content

Commit

Permalink
Allow inline editing of values
Browse files Browse the repository at this point in the history
- Propagate value updates back to the data provider and tracker
- Simplify column declaration and provide dedicated 'edit' property
-- Always render the expander on the first column
- Provide edit renderer for text value changes (more are part of eclipse-cdt-cloud#22)

Refactor:
- Convert some utility functions to React components
- Convert thenable to promises

Closes eclipse-cdt-cloud#16

Co-authored-by: Haydar Metin <[email protected]>
  • Loading branch information
martin-fleck-at and haydar-metin committed Jun 27, 2024
1 parent 05718b2 commit 53bb8fe
Show file tree
Hide file tree
Showing 21 changed files with 438 additions and 159 deletions.
2 changes: 1 addition & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class PeripheralCommands {
await p.updateData();
}
} else {
this.dataTracker.updateData();
return this.dataTracker.updateData();
}
}

Expand Down
21 changes: 21 additions & 0 deletions src/components/tree/components/Codicon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*********************************************************************
* Copyright (c) 2024 Arm Limited and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*********************************************************************/

import React from 'react';


export interface CodiconProps {
icon: string;
}

export const Codicon: React.FC<CodiconProps> = ({ icon }) => {
return <i className={`codicon codicon-${icon}`} style={{ marginRight: '0.5rem' }}></i>;

};
32 changes: 32 additions & 0 deletions src/components/tree/components/Expander.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*********************************************************************
* Copyright (c) 2024 Arm Limited and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*********************************************************************/

import { classNames } from 'primereact/utils';
import React from 'react';

export interface ToggleItem {
expanded?: boolean | undefined;
leaf?: boolean | undefined;
}

export interface ExpandToggleProps {
item: ToggleItem;
depth?: number;
}

export const ExpandToggle: React.FC<ExpandToggleProps> = ({ item, depth = 0 }) => {
return <div
style={{ marginLeft: `${depth * 8}px` }}
className={
classNames('tree-toggler-container', 'codicon', {
'codicon-chevron-down': item.expanded,
'codicon-chevron-right': !item.expanded && !item.leaf,
})} />;
};
38 changes: 38 additions & 0 deletions src/components/tree/components/LabelCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*********************************************************************
* Copyright (c) 2024 Arm Limited and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*********************************************************************/

import React, { HTMLAttributes } from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Tooltip, TooltipContent, TooltipTrigger } from '../../tooltip/tooltip';
import { AsTreeTableCellProps, AsTreeTableCell } from './TreeTableCell';
import { createHighlightedText } from './utils';
import { classNames } from 'primereact/utils';

export interface LabelProps extends AsTreeTableCellProps, HTMLAttributes<HTMLElement> {
}

const Label: React.FC<LabelProps> = React.forwardRef<HTMLDivElement, LabelProps>(({ row: _r, cell, expander: _e, ...props }, ref) => {
const text = createHighlightedText(cell.value, cell.highlight);
const label = <div {...props} ref={ref} className={classNames('tree-label', props.className)}>
{text}
</div>;

if (cell.tooltip === undefined) {
return label;
}

return <Tooltip>
<TooltipTrigger>{label}</TooltipTrigger>
<TooltipContent><Markdown className="markdown" remarkPlugins={[remarkGfm]}>{cell.tooltip}</Markdown></TooltipContent>
</Tooltip>;
});

export const LabelCell = AsTreeTableCell<LabelProps>(Label);
73 changes: 73 additions & 0 deletions src/components/tree/components/TextFieldCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*********************************************************************
* Copyright (c) 2024 Arm Limited and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*********************************************************************/

import './text-field-cell.css';

import { ReactWrapperProps } from '@microsoft/fast-react-wrapper';
import { TextField } from '@vscode/webview-ui-toolkit';
import { VSCodeTextField } from '@vscode/webview-ui-toolkit/react';
import React from 'react';
import { CDTTreeItem, CDTTreeTableColumn } from '../types';
import { AsEditable, EditableComponentProps, EditableComponentRef, AsTreeTableCell } from './TreeTableCell';

export type VSCodeTextFieldComponent = React.Component<ReactWrapperProps<TextField, { onChange: unknown; onInput: unknown; }>, unknown, unknown> & TextField;

const KEY_CHANGE_VALUE = [
'Enter'
];

const KEY_UNSELECT = [
'ArrowUp',
'ArrowDown',
'PageDown',
'PageUp',
'Escape'
];

export interface TextFielCellProps extends EditableComponentProps {
row: CDTTreeItem;
cell: CDTTreeTableColumn;
}

const TextFieldComponent = React.forwardRef<EditableComponentRef, TextFielCellProps>(({ row, cell, ...props }, ref) => {
const textFieldRef = React.useRef<VSCodeTextFieldComponent>(null);

React.useImperativeHandle(ref, () => ({
focus: () => {
textFieldRef.current?.control.select();
}
}));

const onKeyDown = (event: React.KeyboardEvent) => {
event.stopPropagation();

if (KEY_CHANGE_VALUE.includes(event.key)) {
const element = event.currentTarget as HTMLInputElement;
props.onSubmitValue(element.value);
}
if (KEY_UNSELECT.includes(event.key)) {
props.onCancelEdit();
}
};


return <VSCodeTextField
ref={textFieldRef}
className='text-field-cell'
id={`${row.id}-text-field`}
initialValue={cell.value}
value={cell.value}
onKeyDown={event => onKeyDown(event)}
onClick={event => event.stopPropagation()}
onBlur={props.onCancelEdit}
/>;
});

export const TextFieldCell = AsEditable(AsTreeTableCell(TextFieldComponent));
98 changes: 98 additions & 0 deletions src/components/tree/components/TreeTableCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*********************************************************************
* Copyright (c) 2024 Arm Limited and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*********************************************************************/

import React, { ComponentType, HTMLAttributes } from 'react';
import { CDTTreeItem, CDTTreeTableColumn, CTDTreeMessengerType, CTDTreeWebviewContext } from '../types';
import { Codicon } from './Codicon';
import { ExpandToggle } from './Expander';
import { useCDTTreeContext } from '../tree-context';
import { LabelCell } from './LabelCell';
import { classNames } from 'primereact/utils';

export interface AsTreeTableCellProps {
row: CDTTreeItem;
cell: CDTTreeTableColumn;
expander?: boolean;
cellProps?: HTMLAttributes<HTMLElement>;
}

export const AsTreeTableCell = <P extends AsTreeTableCellProps>(Component: ComponentType<P>) => {
const AsTreeTableCell = React.forwardRef<HTMLElement, P & AsTreeTableCellProps>((props, ref) => {
const { cellProps, ...componentProps } = props;
const { row, cell, expander } = componentProps;
return (
<div {...cellProps} className={classNames('treetable-node', 'treetable-cell', cellProps?.className)}
{...CTDTreeWebviewContext.create({ webviewSection: 'tree-item', cdtTreeItemId: row.id, cdtTreeItemPath: row.path, context: row.options?.contextValue })}
>
{expander && <ExpandToggle item={row} depth={row.path.length ?? 1} />}
{cell.icon && <Codicon icon={cell.icon} />}
<Component {...componentProps as P} ref={ref} />
</div>
);
});
return AsTreeTableCell;
};


export interface AsEditableTreeTableCellProps extends AsTreeTableCellProps {
field: string;
}

export interface EditableComponentRef {
focus: () => void;
}

export interface EditableComponentProps {
onCancelEdit: () => void;
onSubmitValue: (value: string) => void;
labelProps?: HTMLAttributes<HTMLElement>;
}

export const AsEditable = <P extends EditableComponentProps>(EditComponent: ComponentType<P>): React.FC<Omit<P, 'onSubmitValue' | 'onCancelEdit'> & AsEditableTreeTableCellProps> => {
return (props) => {
const [isEditMode, setEditMode] = React.useState(false);
const editComponent = React.useRef<EditableComponentRef>(null);
const treeContext = useCDTTreeContext();
const { labelProps, cell, row, expander, field } = props;

React.useEffect(() => {
if (isEditMode && editComponent.current) {
editComponent.current.focus();
}
}, [isEditMode]);

const onStartEdit = (event: React.MouseEvent) => {
setEditMode(true);
event.stopPropagation();
};

const onCancelEdit = () => {
setEditMode(false);
};

const onSubmitValue = (value: string) => {
treeContext.notify(CTDTreeMessengerType.changeValue, {
field,
item: row,
value
});
setEditMode(false);
};

return isEditMode
? <EditComponent
{...(props as unknown as P)}
ref={editComponent}
onCancelEdit={onCancelEdit}
onSubmitValue={onSubmitValue}
/>
: <LabelCell cellProps={{ className: 'editable' }} className='editable' cell={cell} row={row} expander={expander} {...labelProps} onDoubleClick={onStartEdit} onClick={() => { /* capture so no expansion */ }} />;
};
};
28 changes: 28 additions & 0 deletions src/components/tree/components/text-field-cell.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*********************************************************************
* Copyright (c) 2024 Arm Limited and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*********************************************************************/

.text-field-cell {
display: flex;
align-items: center;
background: var(--vscode-active-background);
color: var(--vscode-foreground);
border: var(--vscode-border);
font-family: var(--vscode-editor-font-family);
font-size: 13px;
height: 22px;
line-height: 22px;
padding: 0;
--input-height: 22px;
}

.p-treetable .p-treetable-tbody .treetable-cell.editable:hover .tree-label {
text-decoration: underline dotted var(--vscode-foreground);
text-underline-offset: 4px;
}
4 changes: 4 additions & 0 deletions src/components/tree/components/tree.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,7 @@
overflow: hidden;
align-items: center;
}

.tree-node :first-child {
flex-grow: 1;
}
9 changes: 2 additions & 7 deletions src/components/tree/components/tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import React from 'react';
import { useCDTTreeContext } from '../tree-context';
import { CDTTreeItem, CTDTreeMessengerType, CTDTreeWebviewContext } from '../types';
import { createActions, createHighlightedText, createLabelWithTooltip } from './utils';
import { ExpandToggle } from './Expander';

export type ComponentTreeProps = {
nodes?: CDTTreeItem[];
Expand Down Expand Up @@ -61,13 +62,7 @@ export const ComponentTree = (props: ComponentTreeProps) => {
};

const togglerTemplate = (node: TreeNode) => {
return <div className={
classNames('tree-toggler-container', 'codicon', {
'codicon-chevron-down': node.expanded,
'codicon-chevron-right': !node.expanded && !node.leaf,
})
}>
</div>;
return <ExpandToggle item={node} />;
};

return <div>
Expand Down
Loading

0 comments on commit 53bb8fe

Please sign in to comment.