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

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

Closes eclipse-cdt-cloud#16

Add more edit controls for specific types

- Enumeration Dropdown for fields with enumeration values
- Boolean Checkbox edit for non-enums of width 1

Closes eclipse-cdt-cloud#22

Consider monospace font for values

Closes eclipse-cdt-cloud#45
  • Loading branch information
martin-fleck-at committed Jan 28, 2025
1 parent 14c74a1 commit f56ee0e
Show file tree
Hide file tree
Showing 19 changed files with 606 additions and 265 deletions.
9 changes: 4 additions & 5 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,13 @@ export class PeripheralCommands {

private async peripheralsForceRefresh(node?: PeripheralBaseNode): Promise<void> {
if (node) {
const p = node.getPeripheral();
if (p) {
await p.updateData();
const peripheral = node.getPeripheral();
if (peripheral) {
await peripheral.updateData();
}

this.dataTracker.fireOnDidChange();
} else {
await this.dataTracker.updateData();
return this.dataTracker.updateData();
}
}

Expand Down
33 changes: 33 additions & 0 deletions src/components/tree/components/cells/ActionCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { CDTTreeTableActionColumn, CDTTreeItem, CDTTreeTableActionColumnCommand, CDTTreeItemResource } from '../../types';
import { CommandDefinition } from '../../../../common';

export interface ActionCellProps<T extends CDTTreeItemResource> {
column: CDTTreeTableActionColumn;
record: CDTTreeItem<T>;
actions: CDTTreeTableActionColumnCommand[];
onAction?: (event: React.UIEvent, command: CommandDefinition, value: unknown, record: CDTTreeItem<T>) => void;
}

const ActionCell = <T extends CDTTreeItemResource>({ record, actions, onAction }: ActionCellProps<T>) => {
return (
<div className="tree-actions">
{actions.map((action) => {
const handleAction = (e: React.MouseEvent | React.KeyboardEvent) => onAction?.(e, action, action.value, record);
return (
<i
key={action.commandId}
title={action.title}
className={`codicon codicon-${action.icon}`}
onClick={handleAction}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && handleAction(e)}
/>
);
})}
</div>
);
};

export default ActionCell;
157 changes: 157 additions & 0 deletions src/components/tree/components/cells/EditableStringCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import './editable-string-cell.css';

import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Input, Checkbox, Select } from 'antd';
import type { CheckboxChangeEvent } from 'antd/lib/checkbox';
import LabelCell from './LabelCell';
import { CDTTreeTableStringColumn, CDTTreeItem, EditableEnumData, CDTTreeItemResource } from '../../types';

interface EditableLabelCellProps<T extends CDTTreeItemResource> {
column: CDTTreeTableStringColumn;
record: CDTTreeItem<T>;
editing: boolean;
onSubmit: (newValue: string) => void;
onCancel: () => void;
}

const EditableLabelCell = <T extends CDTTreeItemResource>({
column,
record,
editing,
onSubmit,
onCancel
}: EditableLabelCellProps<T>) => {
const [editMode, setEditMode] = useState(editing);
const [value, setValue] = useState(column.label);
const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const editorRef = useRef<any>(null);

// Focus the editor when entering edit mode.
useEffect(() => {
if (editMode && editorRef.current) {
editorRef.current.focus();
}
}, [editMode]);

const commitEdit = useCallback((newValue: string = value) => {
setValue(newValue);
onSubmit(newValue);
setEditMode(false);
}, [onSubmit, value]);

const cancelEdit = useCallback(() => {
setValue(column.label);
onCancel();
setEditMode(false);
}, [column.label]);

// Cancel the edit only if focus leaves the entire container.
const handleBlur = useCallback(() => {
setTimeout(() => {
if (
containerRef.current &&
document.activeElement &&
!containerRef.current.contains(document.activeElement)
) {
cancelEdit();
}
}, 0);
}, [column.label]);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
cancelEdit();
}
},
[column.label]
);

// Consume the double-click event so no other handler is triggered.
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (column.edit) {
setEditMode(true);
}
}, [column]);

useEffect(() => {
if (editMode || editing) {
editorRef.current && editorRef.current.focus();
}
}, [editMode, editing]);

if (editMode || editing) {
return (
<div className='edit-field-container' ref={containerRef}>
{(() => {
switch (column.edit?.type) {
case 'text':
return (
<Input
ref={editorRef}
className={'text-field-cell'}
value={value}
onChange={e => setValue(e.target.value)}
onPressEnter={e => commitEdit(e.currentTarget.value)}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
/>
);
case 'boolean': {
const checked = value === '1';
return (
<Checkbox
ref={editorRef}
checked={checked}
onChange={(e: CheckboxChangeEvent) => commitEdit(e.target.checked ? '1' : '0')}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
/>
);
}
case 'enum': {
const enumEdit = column.edit as EditableEnumData;
return (
<Select
ref={editorRef}
className={'enum-field-cell'}
placeholder={column.label}
value={value}
onChange={(newValue) => commitEdit(newValue)}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
>
{enumEdit.options.map((opt) => {
const label = opt.value + (opt.detail ? `: ${opt.detail}` : '');
return (
<Select.Option key={opt.value} value={opt.value}>
{label}
</Select.Option>
);
})}
</Select>
);
}
default:
return null;
}
})()}
</div>
);
}

return (
<div
className='editable-string-cell'
onDoubleClick={handleDoubleClick}
style={{ cursor: column.edit ? 'pointer' : 'default' }}
>
<LabelCell record={record} column={column} />
</div>
);
};

export default React.memo(EditableLabelCell);
26 changes: 26 additions & 0 deletions src/components/tree/components/cells/LabelCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import classNames from 'classnames';
import { CDTTreeTableStringColumn, CDTTreeItem, CDTTreeItemResource } from '../../types';
import { createLabelWithTooltip, createHighlightedText } from '../utils';

export interface LabelCellProps<T extends CDTTreeItemResource> {
column: CDTTreeTableStringColumn;
record: CDTTreeItem<T>;
}

const LabelCell = <T extends CDTTreeItemResource>({ column }: LabelCellProps<T>) => {
const icon = column.icon && <i className={classNames('cell-icon', column.icon)} />;

const content = column.tooltip
? createLabelWithTooltip(<span>{createHighlightedText(column.label, column.highlight)}</span>, column.tooltip)
: createHighlightedText(column.label, column.highlight);

return (
<div className="tree-cell ant-table-cell-ellipsis" tabIndex={0}>
{icon}
{content}
</div>
);
};

export default React.memo(LabelCell);
30 changes: 30 additions & 0 deletions src/components/tree/components/cells/StringCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useCallback } from 'react';
import { CDTTreeItem, CDTTreeItemResource, CDTTreeTableStringColumn } from '../../types';
import EditableStringCell from './EditableStringCell';
import LabelCell from './LabelCell';

interface StringCellProps<T extends CDTTreeItemResource> {
column: CDTTreeTableStringColumn;
record: CDTTreeItem<T>;
editing?: boolean;
onSubmit?: (record: CDTTreeItem<T>, newValue: string) => void;
onCancel?: (record: CDTTreeItem<T>) => void;
}

const StringCell = <T extends CDTTreeItemResource>({ column, record, editing = false, onSubmit, onCancel }: StringCellProps<T>) => {
const handleSubmit = useCallback(
(newValue: string) => onSubmit?.(record, newValue),
[record, onSubmit]
);

const handleCancel = useCallback(
() => onCancel?.(record),
[record, onCancel]
);

return column.edit && onSubmit
? <EditableStringCell record={record} column={column} onSubmit={handleSubmit} onCancel={handleCancel} editing={editing} />
: <LabelCell record={record} column={column} />;
};

export default StringCell;
79 changes: 79 additions & 0 deletions src/components/tree/components/cells/editable-string-cell.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*********************************************************************
* 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
*********************************************************************/

.ant-input.text-field-cell {
background: var(--ant-color-bg-container);
color: var(--ant-color-text);
font-family: var(--ant-font-family-code);
font-size: var(--ant-table-cell-font-size);
line-height: var(--ant-line-height);
padding: 1px;
padding-left: 3px;
border-radius: 0;
border-style: dotted;
}

.css-var-r0.ant-select-css-var {
--ant-control-height: auto;
--ant-color-text-placeholder: var(--vscode-sideBar-foreground);
--ant-select-show-arrow-padding-inline-end: 2em;
--ant-border-radius-sm: 0;
--ant-border-radius-lg: 0;
--ant-select-option-active-bg: var(--vscode-list-hoverBackground);
--ant-select-option-font-size: 12px;
--ant-select-option-selected-bg: var(--vscode-list-inactiveSelectionBackground);
--ant-select-option-selected-color: var(--vscode-sideBar-foreground);
--ant-select-option-height: auto;
--ant-select-option-padding: 3px 6px;
}

.enum-field-cell.ant-select-outlined:not(.ant-select-customize-input) .ant-select-selector {
background: var(--ant-color-bg-container);
color: var(--ant-color-text);
font-family: var(--ant-font-family-code);
font-size: var(--ant-table-cell-font-size);
line-height: var(--ant-line-height);
padding: 1px;
padding-left: 3px;
border-radius: 0;
border-style: dotted;
}

.ant-select-dropdown {
background: var(--ant-color-bg-container);
color: var(--ant-color-text);
font-family: var(--ant-font-family-code);
font-size: var(--ant-table-cell-font-size);
line-height: var(--ant-line-height);
padding: 1px;
padding-left: 3px;
}

.enum-field-cell.ant-select-outlined:not(.ant-select-customize-input) .ant-select-arrow {
color: var(--ant-color-text);
}

.editable-string-cell:hover {
text-decoration: underline dotted var(--vscode-foreground);
text-underline-offset: 4px;
}

.ant-table-cell.value {
font-family: var(--ant-font-family-code);
}

.edit-field-container {
display: flex;
flex-grow: 1;
}

.edit-field-container>* {
flex-grow: 1;
}
Loading

0 comments on commit f56ee0e

Please sign in to comment.