mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-19 07:09:28 +00:00
generalized the boards and the libraries views.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
@@ -1,78 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
|
||||
export class ComponentListItem extends React.Component<ComponentListItem.Props> {
|
||||
export class ComponentListItem<T> extends React.Component<ComponentListItem.Props<T>> {
|
||||
|
||||
protected onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
|
||||
const { target } = event.nativeEvent;
|
||||
if (target instanceof HTMLAnchorElement) {
|
||||
this.props.windowService.openNewWindow(target.href);
|
||||
event.nativeEvent.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
protected async install(item: ArduinoComponent): Promise<void> {
|
||||
protected async install(item: T): Promise<void> {
|
||||
await this.props.install(item);
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { item } = this.props;
|
||||
|
||||
const style = ComponentListItem.Styles;
|
||||
const name = <span className={style.NAME_CLASS}>{item.name}</span>;
|
||||
const author = <span className={style.AUTHOR_CLASS}>{item.author}</span>;
|
||||
const installedVersion = !!item.installedVersion && <div className={style.VERSION_INFO_CLASS}>
|
||||
<span className={style.VERSION_CLASS}>Version {item.installedVersion}</span>
|
||||
<span className={style.INSTALLED_CLASS}>INSTALLED</span>
|
||||
</div>;
|
||||
|
||||
const summary = <div className={style.SUMMARY_CLASS}>{item.summary}</div>;
|
||||
|
||||
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
|
||||
const install = this.props.install && item.installable && !item.installedVersion &&
|
||||
<button className={style.INSTALL_BTN_CLASS} onClick={this.install.bind(this, item)}>INSTALL</button>;
|
||||
|
||||
return <div className={[style.LIST_ITEM_CLASS, style.NO_SELECT_CLASS].join(' ')}>
|
||||
<div className={style.HEADER_CLASS}>
|
||||
<span>{name} by {author}</span>
|
||||
{installedVersion}
|
||||
</div>
|
||||
<div className={style.CONTENT_CLASS}>
|
||||
{summary}
|
||||
</div>
|
||||
<div className={style.FOOTER_CLASS}>
|
||||
{moreInfo}
|
||||
{install}
|
||||
</div>
|
||||
</div>;
|
||||
const { item, itemRenderer, install } = this.props;
|
||||
return itemRenderer.renderItem(item, install.bind(this));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace ComponentListItem {
|
||||
|
||||
export interface Props {
|
||||
readonly item: ArduinoComponent;
|
||||
readonly windowService: WindowService;
|
||||
readonly install: (comp: ArduinoComponent) => Promise<void>;
|
||||
}
|
||||
|
||||
export namespace Styles {
|
||||
export const LIST_ITEM_CLASS = 'component-list-item';
|
||||
export const HEADER_CLASS = 'header';
|
||||
export const VERSION_INFO_CLASS = 'version-info';
|
||||
export const CONTENT_CLASS = 'content';
|
||||
export const FOOTER_CLASS = 'footer';
|
||||
export const INSTALLED_CLASS = 'installed';
|
||||
export const NO_SELECT_CLASS = 'noselect';
|
||||
|
||||
export const NAME_CLASS = 'name';
|
||||
export const AUTHOR_CLASS = 'author';
|
||||
export const VERSION_CLASS = 'version';
|
||||
export const SUMMARY_CLASS = 'summary';
|
||||
export const DESCRIPTION_CLASS = 'description';
|
||||
export const INSTALL_BTN_CLASS = 'install';
|
||||
export interface Props<T> {
|
||||
readonly item: T;
|
||||
readonly install: (item: T) => Promise<void>;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { ComponentListItem } from './component-list-item';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
|
||||
export class ComponentList extends React.Component<ComponentList.Props> {
|
||||
export class ComponentList<T> extends React.Component<ComponentList.Props<T>> {
|
||||
|
||||
protected container?: HTMLElement;
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <div
|
||||
className={'items-container'}
|
||||
ref={element => this.container = element || undefined}>
|
||||
ref={this.setRef}>
|
||||
{this.props.items.map(item => this.createItem(item))}
|
||||
</div>;
|
||||
}
|
||||
@@ -21,19 +20,28 @@ export class ComponentList extends React.Component<ComponentList.Props> {
|
||||
}
|
||||
}
|
||||
|
||||
protected createItem(item: ArduinoComponent): React.ReactNode {
|
||||
return <ComponentListItem key={item.name} item={item} windowService={this.props.windowService} install={this.props.install} />
|
||||
protected setRef = (element: HTMLElement | null) => {
|
||||
this.container = element || undefined;
|
||||
}
|
||||
|
||||
protected createItem(item: T): React.ReactNode {
|
||||
return <ComponentListItem<T>
|
||||
key={this.props.itemLabel(item)}
|
||||
item={item}
|
||||
itemRenderer={this.props.itemRenderer}
|
||||
install={this.props.install} />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace ComponentList {
|
||||
|
||||
export interface Props {
|
||||
readonly items: ArduinoComponent[];
|
||||
readonly windowService: WindowService;
|
||||
readonly install: (comp: ArduinoComponent) => Promise<void>;
|
||||
readonly resolveContainer?: (element: HTMLElement) => void;
|
||||
export interface Props<T> {
|
||||
readonly items: T[];
|
||||
readonly itemLabel: (item: T) => string;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly install: (item: T) => Promise<void>;
|
||||
readonly resolveContainer: (element: HTMLElement) => void;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import debounce = require('lodash.debounce');
|
||||
import { Searchable } from '../../../common/protocol/searchable';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { InstallationProgressDialog } from '../installation-progress-dialog';
|
||||
import { SearchBar } from './search-bar';
|
||||
import { ComponentList } from './component-list';
|
||||
import { LibraryService } from '../../../common/protocol/library-service';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { InstallationProgressDialog } from '../installation-progress-dialog';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
|
||||
export class FilterableListContainer extends React.Component<FilterableListContainer.Props, FilterableListContainer.State> {
|
||||
export class FilterableListContainer<T> extends React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
|
||||
|
||||
constructor(props: Readonly<FilterableListContainer.Props>) {
|
||||
constructor(props: Readonly<FilterableListContainer.Props<T>>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
filterText: '',
|
||||
items: []
|
||||
};
|
||||
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount(): void {
|
||||
this.search = debounce(this.search, 500);
|
||||
this.handleFilterTextChange('');
|
||||
}
|
||||
|
||||
@@ -42,36 +43,43 @@ export class FilterableListContainer extends React.Component<FilterableListConta
|
||||
}
|
||||
|
||||
protected renderComponentList(): React.ReactNode {
|
||||
return <ComponentList
|
||||
const { itemLabel, resolveContainer, itemRenderer } = this.props;
|
||||
return <ComponentList<T>
|
||||
items={this.state.items}
|
||||
itemLabel={itemLabel}
|
||||
itemRenderer={itemRenderer}
|
||||
install={this.install.bind(this)}
|
||||
windowService={this.props.windowService}
|
||||
resolveContainer={this.props.resolveContainer}
|
||||
resolveContainer={resolveContainer}
|
||||
/>
|
||||
}
|
||||
|
||||
private handleFilterTextChange(filterText: string): void {
|
||||
const { props } = this.state;
|
||||
this.props.service.search({ query: filterText, props }).then(result => {
|
||||
protected handleFilterTextChange = (filterText: string) => {
|
||||
this.setState({ filterText });
|
||||
this.search(filterText);
|
||||
}
|
||||
|
||||
protected search (query: string): void {
|
||||
const { searchable } = this.props;
|
||||
searchable.search({ query }).then(result => {
|
||||
const { items } = result;
|
||||
this.setState({
|
||||
filterText,
|
||||
items: this.sort(items)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected sort(items: ArduinoComponent[]): ArduinoComponent[] {
|
||||
return items.sort((left, right) => left.name.localeCompare(right.name));
|
||||
protected sort(items: T[]): T[] {
|
||||
const { itemLabel } = this.props;
|
||||
return items.sort((left, right) => itemLabel(left).localeCompare(itemLabel(right)));
|
||||
}
|
||||
|
||||
protected async install(comp: ArduinoComponent): Promise<void> {
|
||||
const dialog = new InstallationProgressDialog(comp.name);
|
||||
protected async install(item: T): Promise<void> {
|
||||
const { installable, searchable, itemLabel } = this.props;
|
||||
const dialog = new InstallationProgressDialog(itemLabel(item));
|
||||
dialog.open();
|
||||
try {
|
||||
await this.props.service.install(comp);
|
||||
const { props } = this.state;
|
||||
const { items } = await this.props.service.search({ query: this.state.filterText, props });
|
||||
await installable.install(item);
|
||||
const { items } = await searchable.search({ query: this.state.filterText });
|
||||
this.setState({ items: this.sort(items) });
|
||||
} finally {
|
||||
dialog.close();
|
||||
@@ -82,23 +90,18 @@ export class FilterableListContainer extends React.Component<FilterableListConta
|
||||
|
||||
export namespace FilterableListContainer {
|
||||
|
||||
export interface Props {
|
||||
readonly service: ComponentSource;
|
||||
readonly windowService: WindowService;
|
||||
readonly resolveContainer?: (element: HTMLElement) => void;
|
||||
readonly resolveFocus?: (element: HTMLElement | undefined) => void;
|
||||
export interface Props<T> {
|
||||
readonly installable: Installable<T>;
|
||||
readonly searchable: Searchable<T>;
|
||||
readonly itemLabel: (item: T) => string;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly resolveContainer: (element: HTMLElement) => void;
|
||||
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
export interface State<T> {
|
||||
filterText: string;
|
||||
items: ArduinoComponent[];
|
||||
props?: LibraryService.Search.Props;
|
||||
}
|
||||
|
||||
export interface ComponentSource {
|
||||
search(req: { query: string, props?: LibraryService.Search.Props }): Promise<{ items: ArduinoComponent[] }>
|
||||
install(board: ArduinoComponent): Promise<void>;
|
||||
items: T[];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListItemRenderer<T> {
|
||||
|
||||
@inject(WindowService)
|
||||
protected windowService: WindowService;
|
||||
|
||||
protected onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
|
||||
const { target } = event.nativeEvent;
|
||||
if (target instanceof HTMLAnchorElement) {
|
||||
this.windowService.openNewWindow(target.href);
|
||||
event.nativeEvent.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
abstract renderItem(item: T, install: (item: T) => Promise<void>): React.ReactNode;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { ListWidget } from './list-widget';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidgetFrontendContribution<T> extends AbstractViewContribution<ListWidget<T>> implements FrontendApplicationContribution {
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import * as React from 'react';
|
||||
import { injectable, postConstruct } from 'inversify';
|
||||
import { Message } from '@phosphor/messaging';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { Searchable } from '../../../common/protocol/searchable';
|
||||
import { FilterableListContainer } from './filterable-list-container';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidget<T> extends ReactWidget {
|
||||
|
||||
/**
|
||||
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
|
||||
*/
|
||||
protected focusNode: HTMLElement | undefined;
|
||||
protected readonly deferredContainer = new Deferred<HTMLElement>();
|
||||
|
||||
constructor(protected options: ListWidget.Options<T>) {
|
||||
super();
|
||||
const { id, label, iconClass } = options;
|
||||
this.id = id;
|
||||
this.title.label = label;
|
||||
this.title.caption = label;
|
||||
this.title.iconClass = iconClass
|
||||
this.title.closable = true;
|
||||
this.addClass('arduino-list-widget');
|
||||
this.node.tabIndex = 0; // To be able to set the focus on the widget.
|
||||
this.scrollOptions = {
|
||||
suppressScrollX: true
|
||||
}
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected getScrollContainer(): MaybePromise<HTMLElement> {
|
||||
return this.deferredContainer.promise;
|
||||
}
|
||||
|
||||
protected onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
(this.focusNode || this.node).focus();
|
||||
}
|
||||
|
||||
protected onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
this.render();
|
||||
}
|
||||
|
||||
protected onFocusResolved = (element: HTMLElement | undefined) => {
|
||||
this.focusNode = element;
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <FilterableListContainer<T>
|
||||
resolveContainer={this.deferredContainer.resolve}
|
||||
resolveFocus={this.onFocusResolved}
|
||||
searchable={this.options.searchable}
|
||||
installable={this.options.installable}
|
||||
itemLabel={this.options.itemLabel}
|
||||
itemRenderer={this.options.itemRenderer} />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace ListWidget {
|
||||
export interface Options<T> {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly iconClass: string;
|
||||
readonly installable: Installable<T>;
|
||||
readonly searchable: Searchable<T>;
|
||||
readonly itemLabel: (item: T) => string;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { AbstractDialog } from "@theia/core/lib/browser";
|
||||
import { AbstractDialog } from '@theia/core/lib/browser';
|
||||
|
||||
export class InstallationProgressDialog extends AbstractDialog<undefined> {
|
||||
|
||||
export class InstallationProgressDialog extends AbstractDialog<string> {
|
||||
readonly value: "does-not-matter";
|
||||
readonly value = undefined;
|
||||
|
||||
constructor(componentName: string) {
|
||||
super({ title: 'Installation in progress' });
|
||||
this.contentNode.textContent = `Installing ${componentName}. Please wait.`;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user