generalized the boards and the libraries views.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta 2019-07-30 16:45:04 +02:00 committed by jbicker
parent b24d440e22
commit 3fcf5a6ac9
24 changed files with 303 additions and 443 deletions

View File

@ -13,7 +13,6 @@ import { ArduinoLanguageGrammarContribution } from './language/arduino-language-
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
import { LibraryListWidgetFrontendContribution } from './library/list-widget-frontend-contribution';
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
@ -52,6 +51,9 @@ import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { SilentScmContribution } from './customization/silent-scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { SilentSearchInWorkspaceContribution } from './customization/silent-search-in-workspace-contribution';
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
import { LibraryItemRenderer } from './library/library-item-renderer';
import { BoardItemRenderer } from './boards/boards-item-renderer';
const ElementQueries = require('css-element-queries/src/ElementQueries');
if (!ARDUINO_PRO_MODE) {
@ -87,6 +89,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
createWidget: () => context.container.get(LibraryListWidget)
}));
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
bind(LibraryItemRenderer).toSelf().inSingletonScope();
// Sketch list service
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
@ -113,6 +116,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
createWidget: () => context.container.get(BoardsListWidget)
}));
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
bind(BoardItemRenderer).toSelf().inSingletonScope();
// Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();

View File

@ -0,0 +1,38 @@
import * as React from 'react';
import { injectable } from 'inversify';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
import { BoardPackage } from '../../common/protocol/boards-service';
@injectable()
export class BoardItemRenderer extends ListItemRenderer<BoardPackage> {
renderItem(item: BoardPackage, install: (item: BoardPackage) => Promise<void>): React.ReactNode {
const name = <span className='name'>{item.name}</span>;
const author = <span className='author'>{item.author}</span>;
const installedVersion = !!item.installedVersion && <div className='version-info'>
<span className='version'>Version {item.installedVersion}</span>
<span className='installed'>INSTALLED</span>
</div>;
const summary = <div className='summary'>{item.summary}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
const installButton = item.installable && !item.installedVersion &&
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
return <div className='component-list-item noselect'>
<div className='header'>
<span>{name} by {author}</span>
{installedVersion}
</div>
<div className='content'>
{summary}
</div>
<div className='footer'>
{moreInfo}
{installButton}
</div>
</div>;
}
}

View File

@ -0,0 +1,27 @@
import { inject, injectable } from 'inversify';
import { BoardPackage, BoardsService } from '../../common/protocol/boards-service';
import { ListWidget } from '../components/component-list/list-widget';
import { BoardItemRenderer } from './boards-item-renderer';
@injectable()
export class BoardsListWidget extends ListWidget<BoardPackage> {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager';
constructor(
@inject(BoardsService) protected service: BoardsService,
@inject(BoardItemRenderer) protected itemRenderer: BoardItemRenderer) {
super({
id: BoardsListWidget.WIDGET_ID,
label: BoardsListWidget.WIDGET_LABEL,
iconClass: 'fa fa-microchip',
searchable: service,
installable: service,
itemLabel: (item: BoardPackage) => item.name,
itemRenderer
});
}
}

View File

@ -1,16 +0,0 @@
import { ListWidget } from './list-widget';
export class BoardsListWidget extends ListWidget {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager';
protected widgetProps(): ListWidget.Props {
return {
id: BoardsListWidget.WIDGET_ID,
title: BoardsListWidget.WIDGET_LABEL,
iconClass: 'fa fa-microchip'
}
}
}

View File

@ -1,22 +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';
import { BoardsListWidget } from './boards-list-widget';
import { MenuModelRegistry } from '@theia/core';
import { BoardsListWidget } from './boards-list-widget';
import { ArduinoMenus } from '../arduino-frontend-contribution';
import { BoardPackage } from '../../common/protocol/boards-service';
import { ListWidgetFrontendContribution } from '../components/component-list/list-widget-frontend-contribution';
@injectable()
export abstract class ListWidgetFrontendContribution extends AbstractViewContribution<ListWidget> implements FrontendApplicationContribution {
async initializeLayout(): Promise<void> {
// await this.openView();
}
}
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution {
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardPackage> {
static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;

View File

@ -1,76 +0,0 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { BoardsService } from '../../common/protocol/boards-service';
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
@injectable()
export abstract class ListWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
constructor() {
super();
const { id, title, iconClass } = this.widgetProps();
this.id = id;
this.title.label = title;
this.title.caption = title;
this.title.iconClass = iconClass;
this.title.closable = true;
this.addClass(ListWidget.Styles.LIST_WIDGET_CLASS);
this.node.tabIndex = 0; // To be able to set the focus on the widget.
}
protected abstract widgetProps(): ListWidget.Props;
@postConstruct()
protected init(): void {
this.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.node.focus();
this.render();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.render();
}
render(): React.ReactNode {
return <FilterableListContainer
service={this.boardsService}
windowService={this.windowService}
/>;
}
}
export namespace ListWidget {
/**
* Props for customizing the abstract list widget.
*/
export interface Props {
readonly id: string;
readonly title: string;
readonly iconClass: string;
}
export namespace Styles {
export const LIST_WIDGET_CLASS = 'arduino-list-widget'
}
}

View File

@ -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>;
}
}

View File

@ -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;
}
}

View File

@ -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[];
}
}

View File

@ -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;
}

View File

@ -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> {
}
}

View File

@ -1,24 +1,16 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
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 { WindowService } from '@theia/core/lib/browser/window/window-service';
import { LibraryFilterableListContainer } from './library-filterable-list-container';
import { LibraryService } from '../../common/protocol/library-service';
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 class LibraryListWidget extends ReactWidget {
static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager';
@inject(LibraryService)
protected readonly libraryService: LibraryService;
@inject(WindowService)
protected readonly windowService: WindowService;
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.
@ -26,12 +18,13 @@ export class LibraryListWidget extends ReactWidget {
protected focusNode: HTMLElement | undefined;
protected readonly deferredContainer = new Deferred<HTMLElement>();
constructor() {
constructor(protected options: ListWidget.Options<T>) {
super();
this.id = LibraryListWidget.WIDGET_ID
this.title.label = LibraryListWidget.WIDGET_LABEL;
this.title.caption = LibraryListWidget.WIDGET_LABEL
this.title.iconClass = 'library-tab-icon';
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.
@ -64,25 +57,25 @@ export class LibraryListWidget extends ReactWidget {
}
render(): React.ReactNode {
return <LibraryFilterableListContainer
return <FilterableListContainer<T>
resolveContainer={this.deferredContainer.resolve}
resolveFocus={this.onFocusResolved}
service={this.libraryService}
windowService={this.windowService}
/>;
searchable={this.options.searchable}
installable={this.options.installable}
itemLabel={this.options.itemLabel}
itemRenderer={this.options.itemRenderer} />;
}
}
export namespace ListWidget {
/**
* Props for customizing the abstract list widget.
*/
export interface Props {
export interface Options<T> {
readonly id: string;
readonly title: string;
readonly label: string;
readonly iconClass: string;
readonly installable: Installable<T>;
readonly searchable: Searchable<T>;
readonly itemLabel: (item: T) => string;
readonly itemRenderer: ListItemRenderer<T>;
}
}

View File

@ -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.`;
}
}
}

View File

@ -1,50 +0,0 @@
import * as React from 'react';
import { ComponentListItem } from '../components/component-list/component-list-item';
export class LibraryComponentListItem extends ComponentListItem {
render(): React.ReactNode {
const { item } = this.props;
const name = <span className={'name'}>{item.name}</span>;
const author = <span className={'author'}>by {item.author}</span>;
const installedVersion = !!item.installedVersion && <div className={'version-info'}>
<span className={'version'}>Version {item.installedVersion}</span>
<span className={'installed'}>INSTALLED</span>
</div>;
const summary = <div className={'summary'}>{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={'install'} onClick={this.install.bind(this, item)}>INSTALL</button>;
const versions = (() => {
const { availableVersions } = item;
if (availableVersions.length === 0) {
return undefined;
} else if (availableVersions.length === 1) {
return <label>{availableVersions[0]}</label>
} else {
return <select>{item.availableVersions.map(version => <option value={version} key={version}>{version}</option>)}</select>;
}
})();
return <div className={'component-list-item noselect'}>
<div className={'header'}>
<span>{name} {author}</span>
{installedVersion}
</div>
<div className={'content'}>
{summary}
</div>
<div className={'info'}>
{moreInfo}
</div>
<div className={'footer'}>
{install}
{versions}
</div>
</div>;
}
}

View File

@ -1,17 +0,0 @@
import * as React from 'react';
import { ArduinoComponent } from '../../common/protocol/arduino-component';
import { ComponentList } from '../components/component-list/component-list';
import { LibraryComponentListItem } from './library-component-list-item';
export class LibraryComponentList extends ComponentList {
createItem(item: ArduinoComponent): React.ReactNode {
return <LibraryComponentListItem
key={item.name}
item={item}
windowService={this.props.windowService}
install={this.props.install}
/>
}
}

View File

@ -1,110 +0,0 @@
import * as React from 'react';
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
import { LibraryComponentList } from './library-component-list';
export class LibraryFilterableListContainer extends FilterableListContainer {
constructor(props: Readonly<FilterableListContainer.Props>) {
super(props);
this.state = {
filterText: '',
items: [],
props: {
topic: this.topics[0],
type: this.types[0]
}
};
}
protected renderSearchFilter(): React.ReactNode {
const types = this.types.map(type => <option value={type} key={type}>{type}</option>);
let type = this.types[0];
if (this.state.props) {
const currentType = this.types.find(t => t === this.state.props!.type) || this.types[0];
if (currentType) {
type = currentType;
}
}
const topics = this.topics.map(topic => <option value={topic} key={topic}>{topic}</option>);
let topic = this.topics[0];
if (this.state.props) {
const currentTopic = this.topics.find(t => t === this.state.props!.topic) || this.topics[0];
if (currentTopic) {
topic = currentTopic;
}
}
return <div className={'search-filters'}>
<div className={'filter'}>
<div className={'title'} style={{ minWidth: '32.088px' }}>Type</div> {/** TODO: do `minWidth` better! */}
<select
value={type}
onChange={this.onTypeChange}>
{types}
</select>
</div>
<div className={'filter'}>
<div className={'title'}>Topic</div>
<select
value={topic}
onChange={this.onTopicChange}>
{topics}
</select>
</div>
</div>
}
protected onTypeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const type = event.target.value;
const props = { ...(this.state.props || {}), ...{ type } };
this.setState({
props
});
}
protected onTopicChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const topic = event.target.value;
const props = { ...(this.state.props || {}), ...{ topic } };
this.setState({
props
});
}
protected renderComponentList(): React.ReactNode {
return <LibraryComponentList
items={this.state.items}
install={this.install.bind(this)}
windowService={this.props.windowService}
resolveContainer={this.props.resolveContainer}
/>
}
private get topics(): string[] {
return [
'All',
'Communication',
'Data Processing',
'Data Storage',
'Device Control',
'Display',
'Other',
'Sensor',
'Signal Input/Output',
'Timing',
'Uncategorized'
];
}
private get types(): string[] {
return [
'All',
'Updatable',
'Installed',
'Arduino',
'Partner',
'Recommended',
'Contributed',
'Retired'
];
}
}

View File

@ -0,0 +1,52 @@
import * as React from 'react';
import { injectable } from 'inversify';
import { Library } from '../../common/protocol/library-service';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
@injectable()
export class LibraryItemRenderer extends ListItemRenderer<Library> {
renderItem(item: Library, install: (item: Library) => Promise<void>): React.ReactNode {
const name = <span className='name'>{item.name}</span>;
const author = <span className='author'>by {item.author}</span>;
const installedVersion = !!item.installedVersion && <div className='version-info'>
<span className='version'>Version {item.installedVersion}</span>
<span className='installed'>INSTALLED</span>
</div>;
const summary = <div className='summary'>{item.summary}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
const installButton = item.installable && !item.installedVersion &&
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
const versions = (() => {
const { availableVersions } = item;
if (availableVersions.length === 0) {
return undefined;
} else if (availableVersions.length === 1) {
return <label>{availableVersions[0]}</label>
} else {
return <select>{item.availableVersions.map(version => <option value={version} key={version}>{version}</option>)}</select>;
}
})();
return <div className='component-list-item noselect'>
<div className='header'>
<span>{name} {author}</span>
{installedVersion}
</div>
<div className='content'>
{summary}
</div>
<div className='info'>
{moreInfo}
</div>
<div className='footer'>
{installButton}
{versions}
</div>
</div>;
}
}

View File

@ -0,0 +1,27 @@
import { inject, injectable } from 'inversify';
import { Library, LibraryService } from '../../common/protocol/library-service';
import { ListWidget } from '../components/component-list/list-widget';
import { LibraryItemRenderer } from './library-item-renderer';
@injectable()
export class LibraryListWidget extends ListWidget<Library> {
static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager';
constructor(
@inject(LibraryService) protected service: LibraryService,
@inject(LibraryItemRenderer) protected itemRenderer: LibraryItemRenderer) {
super({
id: LibraryListWidget.WIDGET_ID,
label: LibraryListWidget.WIDGET_LABEL,
iconClass: 'library-tab-icon',
searchable: service,
installable: service,
itemLabel: (item: Library) => item.name,
itemRenderer
});
}
}

View File

@ -1,5 +1,7 @@
import { ArduinoComponent } from "./arduino-component";
import { JsonRpcServer } from "@theia/core";
import { JsonRpcServer } from '@theia/core';
import { Searchable } from './searchable';
import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component';
export interface AttachedBoardsChangeEvent {
readonly oldState: Readonly<{ boards: Board[] }>;
@ -18,10 +20,8 @@ export interface BoardsServiceClient {
export const BoardsServicePath = '/services/boards-service';
export const BoardsService = Symbol('BoardsService');
export interface BoardsService extends JsonRpcServer<BoardsServiceClient> {
export interface BoardsService extends Installable<BoardPackage>, Searchable<BoardPackage>, JsonRpcServer<BoardsServiceClient> {
getAttachedBoards(): Promise<{ boards: Board[] }>;
search(options: { query?: string }): Promise<{ items: BoardPackage[] }>;
install(item: BoardPackage): Promise<void>;
}
export interface BoardPackage extends ArduinoComponent {

View File

@ -0,0 +1,3 @@
export interface Installable<T> {
install(item: T): Promise<void>;
}

View File

@ -1,20 +1,13 @@
import { ArduinoComponent } from "./arduino-component";
import { Searchable } from './searchable';
import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component';
export const LibraryServicePath = '/services/library-service';
export const LibraryService = Symbol('LibraryService');
export interface LibraryService {
search(options: { query?: string, props?: LibraryService.Search.Props }): Promise<{ items: Library[] }>;
export interface LibraryService extends Installable<Library>, Searchable<Library> {
install(library: Library): Promise<void>;
}
export namespace LibraryService {
export namespace Search {
export interface Props {
[key: string]: string | undefined;
}
}
}
export interface Library extends ArduinoComponent {
readonly builtIn?: boolean;
}

View File

@ -0,0 +1,11 @@
export interface Searchable<T> {
search(options: Searchable.Options): Promise<{ items: T[] }>;
}
export namespace Searchable {
export interface Options {
/**
* Defaults to empty an empty string.
*/
readonly query?: string;
}
}

View File

@ -14,7 +14,7 @@ export class LibraryServiceImpl implements LibraryService {
@inject(ToolOutputServiceServer)
protected readonly toolOutputService: ToolOutputServiceServer;
async search(options: { query?: string, props: LibraryService.Search.Props }): Promise<{ items: Library[] }> {
async search(options: { query?: string }): Promise<{ items: Library[] }> {
const coreClient = await this.coreClientProvider.getClient();
if (!coreClient) {
return { items: [] };