{
+ private subscription?: Subscription;
+
+ public componentDidMount() {
+ const { applicationsStore, filterStore, tabStore } = this.props;
+ this.subscription = tabStore?.action.subscribe({
+ next: (action) => {
+ const { categories } = applicationsStore!;
+ if (categories.length === 0) {
+ filterStore?.clear();
+
+ return;
+ }
+
+ const { filter } = filterStore!;
+ const index = categories.findIndex((category) => category === filter);
+ switch (action) {
+ case "next": {
+ const newIndex = Math.min(categories.length - 1, index + 1);
+ filterStore?.update(categories[newIndex]);
+ break;
+ }
+ case "previous": {
+ const newIndex = index - 1;
+ if (newIndex < 0) {
+ filterStore?.clear();
+ } else {
+ filterStore?.update(categories[newIndex]);
+ }
+ break;
+ }
+ }
+ },
+ });
+ }
+
+ public componentWillUnmount() {
+ if (this.subscription !== undefined) {
+ this.subscription.unsubscribe();
+ this.subscription = undefined;
+ }
+ }
+
+ public render() {
+ const { applicationsStore, classes, filterStore } = this.props;
+
+ const { categories } = applicationsStore!;
+
+ const { filter = "all" } = filterStore!;
+
+ return (
+
+
+
+ {categories.map((category) => {
+ return ;
+ })}
+
+
+ );
+ }
+
+ private readonly onChange = (_: React.ChangeEvent, value: string) => {
+ const { filterStore } = this.props;
+ if (value === "all") {
+ filterStore?.clear();
+ } else {
+ filterStore?.update(value);
+ }
+ };
+}
+
+export default withStyles(styles)(CategoryTabs);
diff --git a/packages/desktop-dock/src/components/Search/Search.tsx b/packages/desktop-dock/src/components/Search/Search.tsx
index 277439a3..5efb3ce2 100644
--- a/packages/desktop-dock/src/components/Search/Search.tsx
+++ b/packages/desktop-dock/src/components/Search/Search.tsx
@@ -1,15 +1,13 @@
-import { Grid } from "@material-ui/core";
import * as React from "react";
-import { SearchButton } from "./SearchButton";
-import { SearchInput } from "./SearchInput";
+import { SearchAutocompleteContainer } from "./SearchAutocompleteContainer";
-export class Search extends React.PureComponent {
+interface ISearchProps {
+ readonly endAdornment?: React.ReactNode;
+ readonly startAdornment?: React.ReactNode;
+}
+
+export class Search extends React.PureComponent {
public render() {
- return (
-
-
-
-
- );
+ return ;
}
}
diff --git a/packages/desktop-dock/src/components/Search/SearchAutocomplete.tsx b/packages/desktop-dock/src/components/Search/SearchAutocomplete.tsx
new file mode 100644
index 00000000..b9095ad7
--- /dev/null
+++ b/packages/desktop-dock/src/components/Search/SearchAutocomplete.tsx
@@ -0,0 +1,171 @@
+// Copyright © 2020 Reactive Markets. All rights reserved.
+
+import { createStyles, InputAdornment, TextField, Theme, withStyles, WithStyles } from "@material-ui/core";
+import { FilterOptionsState } from "@material-ui/lab";
+import Autocomplete, {
+ AutocompleteInputChangeReason,
+ AutocompleteRenderInputParams,
+ AutocompleteRenderOptionState,
+} from "@material-ui/lab/Autocomplete";
+import { matchSorter, MatchSorterOptions } from "match-sorter";
+import * as React from "react";
+import { IApplication } from "../../stores";
+import CategoryTabs from "./CategoryTabs";
+import SearchRenderOption from "./SearchRenderOption";
+import { VirtualizedListbox } from "./VirtualizedListbox";
+
+const matchOptions: MatchSorterOptions = {
+ keys: ["display", "name", "description", "category"],
+};
+
+const filterOptions = (options: IApplication[], state: FilterOptionsState): IApplication[] => {
+ return state.inputValue
+ .split(" ")
+ .reduceRight((results, term) => matchSorter(results, term, matchOptions), options);
+};
+
+const styles = (theme: Theme) =>
+ createStyles({
+ display: {
+ display: "flex",
+ flexDirection: "column",
+ },
+ listbox: {
+ padding: 0,
+ maxHeight: "unset",
+ },
+ noOptions: {
+ fontSize: 13,
+ paddingLeft: 16,
+ },
+ option: {
+ minHeight: "auto",
+ padding: 0,
+ '&[aria-selected="true"]': {
+ backgroundColor: "transparent",
+ },
+ '&[data-focus="true"]': {
+ backgroundColor: theme.palette.action.hover,
+ },
+ },
+ paper: {
+ backgroundColor: "transparent",
+ borderRadius: 0,
+ boxShadow: "none",
+ margin: 0,
+ height: "100%",
+ },
+ popper: {
+ borderTop: `1px solid ${theme.palette.divider}`,
+ height: "100%",
+ width: "100%",
+ },
+ popperDisablePortal: {
+ position: "relative",
+ width: "auto !important",
+ },
+ root: {
+ width: "100%",
+ },
+ textField: {
+ padding: "14px 8px 14px 0",
+ },
+ });
+
+interface SearchAutocompleteProps extends WithStyles {
+ readonly endAdornment?: React.ReactNode;
+ readonly inputRef: React.RefObject;
+ readonly inputValue?: string;
+ readonly onInputChange?: (
+ event: React.ChangeEvent,
+ value: string,
+ reason: AutocompleteInputChangeReason,
+ ) => void;
+ readonly onKeyDown?: (event: React.KeyboardEvent) => void;
+ readonly options: IApplication[];
+ readonly startAdornment?: React.ReactNode;
+ readonly value?: IApplication;
+ onChange(selected: IApplication): void;
+}
+
+class SearchAutocomplete extends React.PureComponent {
+ public render() {
+ const { classes, inputRef, inputValue = "", onKeyDown, onInputChange, options, value } = this.props;
+
+ return (
+ >}
+ noOptionsText="No results."
+ onChange={this.onChange}
+ onKeyDown={onKeyDown}
+ onInputChange={onInputChange}
+ open
+ options={options}
+ getOptionLabel={this.getOptionLabel}
+ getOptionSelected={this.getOptionSelected}
+ ref={inputRef}
+ renderInput={this.renderInput}
+ renderOption={this.renderOption}
+ value={value}
+ />
+ );
+ }
+
+ private readonly getOptionLabel = (option: IApplication) => {
+ return option.display ?? option.name;
+ };
+
+ private readonly getOptionSelected = (option: IApplication, value: IApplication) => {
+ return option.key === value.key;
+ };
+
+ private readonly renderInput = (params: AutocompleteRenderInputParams) => {
+ const { classes, endAdornment, startAdornment } = this.props;
+
+ return (
+ <>
+ {endAdornment},
+ ref: params.InputProps.ref,
+ startAdornment: {startAdornment},
+ }}
+ placeholder="Search"
+ />
+
+ >
+ );
+ };
+
+ private readonly renderOption = (option: IApplication, { selected }: AutocompleteRenderOptionState) => {
+ return ;
+ };
+
+ public readonly onChange = (_: React.ChangeEvent, selected: IApplication | null) => {
+ if (selected !== null) {
+ this.props.onChange(selected);
+ }
+ };
+}
+
+export default withStyles(styles)(SearchAutocomplete);
diff --git a/packages/desktop-dock/src/components/Search/SearchAutocompleteContainer.tsx b/packages/desktop-dock/src/components/Search/SearchAutocompleteContainer.tsx
new file mode 100644
index 00000000..5fe3cb2e
--- /dev/null
+++ b/packages/desktop-dock/src/components/Search/SearchAutocompleteContainer.tsx
@@ -0,0 +1,128 @@
+import { AutocompleteInputChangeReason } from "@material-ui/lab";
+import { reaction } from "mobx";
+import { observer, inject, disposeOnUnmount } from "mobx-react";
+import * as React from "react";
+import {
+ IApplication,
+ IApplicationsStore,
+ IFilterStore,
+ IFocusStore,
+ IResizerStore,
+ ISearchStore,
+ ITabStore,
+} from "../../stores";
+import SearchAutocomplete from "./SearchAutocomplete";
+
+interface ISearchAutocompleteContainerProps {
+ readonly applicationsStore?: IApplicationsStore;
+ readonly endAdornment?: React.ReactNode;
+ readonly filterStore?: IFilterStore;
+ readonly focusStore?: IFocusStore;
+ readonly resizerStore?: IResizerStore;
+ readonly searchStore?: ISearchStore;
+ readonly startAdornment?: React.ReactNode;
+ readonly tabStore?: ITabStore;
+}
+
+@inject("applicationsStore")
+@inject("filterStore")
+@inject("focusStore")
+@inject("resizerStore")
+@inject("searchStore")
+@inject("tabStore")
+@observer
+export class SearchAutocompleteContainer extends React.Component {
+ private readonly expandDelay = 500;
+ private readonly ref = React.createRef();
+
+ public componentDidMount() {
+ this.props.focusStore?.on("focus-input", this.focus);
+
+ disposeOnUnmount(
+ this,
+ reaction(
+ () => this.props.searchStore?.term,
+ (term) => {
+ if (term !== "") {
+ this.props.resizerStore?.expand();
+ }
+ },
+ { delay: this.expandDelay },
+ ),
+ );
+ }
+
+ public componentWillUnmount() {
+ this.props.focusStore?.off("focus-input", this.focus);
+ }
+
+ public render() {
+ const { applicationsStore, endAdornment, filterStore, searchStore, startAdornment } = this.props;
+
+ const { applications } = applicationsStore!;
+
+ const { filter } = filterStore!;
+
+ const options = filter === undefined ? applications.slice() : applications.filter((a) => a.category === filter);
+
+ const { term } = searchStore!;
+
+ return (
+
+ );
+ }
+
+ private readonly focus = () => {
+ this.ref.current?.focus();
+ };
+
+ private readonly onChange = async (selected: IApplication) => {
+ try {
+ await this.props.applicationsStore?.launch(selected);
+ } catch (error) {
+ console.error(`Failed to launch application: ${error}`);
+ }
+ };
+
+ private readonly onInputChange = (
+ _: React.ChangeEvent,
+ value: string,
+ reason: AutocompleteInputChangeReason,
+ ) => {
+ const { searchStore } = this.props;
+ switch (reason) {
+ case "input":
+ searchStore?.update(value);
+ break;
+ default:
+ searchStore?.clear();
+ break;
+ }
+ };
+
+ private readonly onKeyDown = (event: React.KeyboardEvent) => {
+ const { filterStore, resizerStore, searchStore, tabStore } = this.props;
+ switch (event.key) {
+ case "ArrowLeft":
+ tabStore?.previous();
+ break;
+ case "ArrowRight":
+ tabStore?.next();
+ break;
+ case "Escape":
+ resizerStore?.collapse();
+ filterStore?.clear();
+ searchStore?.clear();
+ break;
+ }
+ };
+}
diff --git a/packages/desktop-dock/src/components/Search/SearchButton.tsx b/packages/desktop-dock/src/components/Search/SearchButton.tsx
index f4f0b31d..a579f953 100644
--- a/packages/desktop-dock/src/components/Search/SearchButton.tsx
+++ b/packages/desktop-dock/src/components/Search/SearchButton.tsx
@@ -2,17 +2,15 @@ import { IconButton, Tooltip } from "@material-ui/core";
import { Magnify } from "mdi-material-ui";
import { inject } from "mobx-react";
import * as React from "react";
-import { IFocusStore, ISearchStore, IResizerStore } from "../../stores";
+import { IFocusStore, IResizerStore } from "../../stores";
interface ISearchButtonProps {
readonly focusStore?: IFocusStore;
readonly resizerStore?: IResizerStore;
- readonly searchStore?: ISearchStore;
}
@inject("focusStore")
@inject("resizerStore")
-@inject("searchStore")
export class SearchButton extends React.PureComponent {
public render() {
return (
@@ -25,7 +23,6 @@ export class SearchButton extends React.PureComponent {
}
private readonly onClick = () => {
- this.props.searchStore?.search();
this.props.resizerStore?.expand();
this.props.focusStore?.focusInput();
};
diff --git a/packages/desktop-dock/src/components/Search/SearchCategories.tsx b/packages/desktop-dock/src/components/Search/SearchCategories.tsx
deleted file mode 100644
index 87d27b01..00000000
--- a/packages/desktop-dock/src/components/Search/SearchCategories.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Box } from "@material-ui/core";
-import * as React from "react";
-import AutoSizer from "react-virtualized-auto-sizer";
-import { SearchResultList } from "./SearchResultList";
-
-export class SearchCategories extends React.PureComponent {
- public render() {
- return (
-
- {(size) => }
-
- );
- }
-}
diff --git a/packages/desktop-dock/src/components/Search/SearchCategoryItem.tsx b/packages/desktop-dock/src/components/Search/SearchCategoryItem.tsx
deleted file mode 100644
index a9e0280e..00000000
--- a/packages/desktop-dock/src/components/Search/SearchCategoryItem.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { ListItem, ListItemText } from "@material-ui/core";
-import { launcher } from "@reactivemarkets/desktop-sdk";
-import * as React from "react";
-import { ListChildComponentProps } from "react-window";
-import { IApplication } from "../../stores";
-
-export class SearchCategoryItem extends React.Component {
- public render() {
- const { data, index, style } = this.props;
-
- const { configuration } = data[index] as IApplication;
-
- const { name } = configuration.metadata;
-
- return (
-
-
-
- );
- }
-
- private readonly onClick = async () => {
- try {
- const { data, index } = this.props;
-
- const { configuration } = data[index] as IApplication;
-
- await launcher.launch(configuration);
- } catch (error) {
- console.error(`Failed to launch application: ${error}`);
- }
- };
-}
diff --git a/packages/desktop-dock/src/components/Search/SearchCategoryList.tsx b/packages/desktop-dock/src/components/Search/SearchCategoryList.tsx
deleted file mode 100644
index 4cf9aacc..00000000
--- a/packages/desktop-dock/src/components/Search/SearchCategoryList.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { inject, observer } from "mobx-react";
-import * as React from "react";
-import { FixedSizeList } from "react-window";
-import { IApplicationsStore } from "../../stores";
-import SearchResultItem from "./SearchResultItem";
-
-interface ISearchCategoryListProps {
- readonly applicationsStore?: IApplicationsStore;
- readonly height: number;
- readonly width: number;
-}
-
-@inject("applicationsStore")
-@observer
-export class SearchCategoryList extends React.Component {
- public componentDidMount() {
- this.props.applicationsStore?.load();
- }
-
- public render() {
- const { applicationsStore, height, width } = this.props;
-
- const { applications } = applicationsStore!;
-
- return (
-
- {SearchResultItem}
-
- );
- }
-}
diff --git a/packages/desktop-dock/src/components/Search/SearchHelp.tsx b/packages/desktop-dock/src/components/Search/SearchHelp.tsx
index 33e13c91..acb23df7 100644
--- a/packages/desktop-dock/src/components/Search/SearchHelp.tsx
+++ b/packages/desktop-dock/src/components/Search/SearchHelp.tsx
@@ -1,5 +1,5 @@
import { createStyles, Grid, Theme, Typography, withStyles, WithStyles } from "@material-ui/core";
-import { ArrowUp, ArrowDown, KeyboardEsc, KeyboardReturn } from "mdi-material-ui";
+import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight, KeyboardEsc, KeyboardReturn } from "mdi-material-ui";
import * as React from "react";
import Key from "./Key";
@@ -15,7 +15,7 @@ const styles = (theme: Theme) =>
root: {
borderTop: `1px solid ${theme.palette.divider}`,
margin: 0,
- padding: 0,
+ padding: "0 0 0 8px",
},
text: {
marginLeft: 4,
@@ -27,7 +27,7 @@ class SearchHelp extends React.PureComponent> {
const { classes } = this.props;
return (
-
+
@@ -35,10 +35,21 @@ class SearchHelp extends React.PureComponent> {
-
+
Select results
+
+
+
+
+
+
+
+
+ Switch category
+
+
diff --git a/packages/desktop-dock/src/components/Search/SearchInput.tsx b/packages/desktop-dock/src/components/Search/SearchInput.tsx
deleted file mode 100644
index 3bd6f1ad..00000000
--- a/packages/desktop-dock/src/components/Search/SearchInput.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { TextField } from "@material-ui/core";
-import { reaction } from "mobx";
-import { observer, inject, disposeOnUnmount } from "mobx-react";
-import * as React from "react";
-import { IFocusStore, IResizerStore, ISearchStore, IApplication } from "../../stores";
-
-interface ISearchInputProps {
- readonly focusStore?: IFocusStore;
- readonly resizerStore?: IResizerStore;
- readonly searchStore?: ISearchStore;
-}
-
-@inject("focusStore")
-@inject("resizerStore")
-@inject("searchStore")
-@observer
-export class SearchInput extends React.Component {
- private readonly expandDelay = 500;
- private readonly ref = React.createRef();
-
- public componentDidMount() {
- this.props.focusStore?.on("focus-input", this.focus);
-
- disposeOnUnmount(
- this,
- reaction(
- () => this.props.searchStore?.searchTerm,
- (searchTerm) => {
- if (searchTerm !== "") {
- this.props.resizerStore?.expand();
- }
- },
- { delay: this.expandDelay },
- ),
- );
- }
-
- public componentWillUnmount() {
- this.props.focusStore?.off("focus-input", this.focus);
- }
-
- public render() {
- const { searchStore } = this.props;
-
- const { searchTerm } = searchStore!;
-
- return (
-
- );
- }
-
- private readonly focus = () => {
- this.ref.current?.focus();
- };
-
- private readonly onChange = (event: React.ChangeEvent) => {
- this.props.searchStore?.search(event.target.value);
- };
-
- private readonly onKeyDown = (event: React.KeyboardEvent) => {
- switch (event.key) {
- case "Escape":
- this.props.searchStore?.clear();
- this.props.resizerStore?.collapse();
- break;
- case "Enter": {
- const { results } = this.props.searchStore!;
- if (results.length > 0) {
- this.launch(results[0].item);
- }
- break;
- }
- }
- };
-
- private readonly launch = async (application: IApplication) => {
- try {
- await application.launch();
- } catch (error) {
- console.error(`Failed to launch application: ${error}`);
- }
- };
-}
diff --git a/packages/desktop-dock/src/components/Search/SearchResultItem.tsx b/packages/desktop-dock/src/components/Search/SearchRenderOption.tsx
similarity index 60%
rename from packages/desktop-dock/src/components/Search/SearchResultItem.tsx
rename to packages/desktop-dock/src/components/Search/SearchRenderOption.tsx
index 954656f9..842e8cc4 100644
--- a/packages/desktop-dock/src/components/Search/SearchResultItem.tsx
+++ b/packages/desktop-dock/src/components/Search/SearchRenderOption.tsx
@@ -10,8 +10,7 @@ import {
import { Close } from "mdi-material-ui";
import { inject } from "mobx-react";
import * as React from "react";
-import { ListChildComponentProps } from "react-window";
-import { IApplicationsStore, ISearchResult, ISearchStore } from "../../stores";
+import { IApplicationsStore, IApplication } from "../../stores";
import { ConfirmButton } from "../System";
import SearchResultIcon from "./SearchResultIcon";
@@ -21,34 +20,35 @@ const styles = () =>
"&:hover $clearIndicator": {
visibility: "visible",
},
+ width: "100%",
},
clearIndicator: {
visibility: "hidden",
},
});
-interface ISearchResultItemProps extends ListChildComponentProps, WithStyles {
+interface ISearchRenderOptionProps extends WithStyles {
readonly applicationsStore?: IApplicationsStore;
- readonly searchStore?: ISearchStore;
+ readonly result: IApplication;
+ readonly selected?: boolean;
}
@inject("applicationsStore")
-@inject("searchStore")
-class SearchResultItem extends React.Component {
+class SearchRenderOption extends React.PureComponent {
public render() {
- const { classes, data, index, style } = this.props;
+ const { classes, result, selected } = this.props;
- const { item } = data[index] as ISearchResult;
+ const { display, name, icon, description } = result;
- const { name, icon, description } = item;
+ const primary = display ?? name;
return (
{icon && (
@@ -56,7 +56,14 @@ class SearchResultItem extends React.Component {
)}
-
+
@@ -68,11 +75,9 @@ class SearchResultItem extends React.Component {
private readonly onClick = async () => {
try {
- const { data, index } = this.props;
+ const { applicationsStore, result } = this.props;
- const { item } = data[index] as ISearchResult;
-
- await item.launch();
+ await applicationsStore?.launch(result);
} catch (error) {
console.error(`Failed to launch application: ${error}`);
}
@@ -80,17 +85,13 @@ class SearchResultItem extends React.Component {
private readonly remove = async () => {
try {
- const { applicationsStore, data, index, searchStore } = this.props;
-
- const { item } = data[index] as ISearchResult;
-
- await applicationsStore!.remove(item);
+ const { applicationsStore, result } = this.props;
- searchStore!.search(searchStore!.searchTerm);
+ await applicationsStore?.remove(result);
} catch (error) {
console.error(`Failed to remove application: ${error}`);
}
};
}
-export default withStyles(styles)(SearchResultItem);
+export default withStyles(styles)(SearchRenderOption);
diff --git a/packages/desktop-dock/src/components/Search/SearchResultIcon.tsx b/packages/desktop-dock/src/components/Search/SearchResultIcon.tsx
index d4542c11..1ae63c39 100644
--- a/packages/desktop-dock/src/components/Search/SearchResultIcon.tsx
+++ b/packages/desktop-dock/src/components/Search/SearchResultIcon.tsx
@@ -16,8 +16,8 @@ const styles = () =>
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
- width: 16,
- height: 16,
+ width: 24,
+ height: 24,
},
});
@@ -35,7 +35,7 @@ class SearchResultIcon extends React.PureComponent {
return (
-
+
);
}
diff --git a/packages/desktop-dock/src/components/Search/SearchResultList.tsx b/packages/desktop-dock/src/components/Search/SearchResultList.tsx
deleted file mode 100644
index 73cad638..00000000
--- a/packages/desktop-dock/src/components/Search/SearchResultList.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { inject, observer } from "mobx-react";
-import * as React from "react";
-import { FixedSizeList } from "react-window";
-import { ISearchStore } from "../../stores";
-import SearchResultItem from "./SearchResultItem";
-
-interface ISearchResultListProps {
- readonly height: number;
- readonly searchStore?: ISearchStore;
- readonly width: number;
-}
-
-@inject("searchStore")
-@observer
-export class SearchResultList extends React.Component {
- public render() {
- const { height, searchStore, width } = this.props;
-
- const { results } = searchStore!;
-
- return (
-
- {SearchResultItem}
-
- );
- }
-}
diff --git a/packages/desktop-dock/src/components/Search/SearchResults.tsx b/packages/desktop-dock/src/components/Search/SearchResults.tsx
deleted file mode 100644
index 91106aad..00000000
--- a/packages/desktop-dock/src/components/Search/SearchResults.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Box } from "@material-ui/core";
-import * as React from "react";
-import AutoSizer from "react-virtualized-auto-sizer";
-import { SearchResultList } from "./SearchResultList";
-
-export class SearchResults extends React.PureComponent {
- public render() {
- return (
-
- {(size) => }
-
- );
- }
-}
diff --git a/packages/desktop-dock/src/components/Search/VirtualizedListbox.tsx b/packages/desktop-dock/src/components/Search/VirtualizedListbox.tsx
new file mode 100644
index 00000000..39af4162
--- /dev/null
+++ b/packages/desktop-dock/src/components/Search/VirtualizedListbox.tsx
@@ -0,0 +1,47 @@
+import * as React from "react";
+import AutoSizer from "react-virtualized-auto-sizer";
+import { ListChildComponentProps, FixedSizeList } from "react-window";
+
+const renderRow = (props: ListChildComponentProps) => {
+ const { data, index, style } = props;
+
+ return React.cloneElement(data[index], {
+ style,
+ });
+};
+
+const OuterElementContext = React.createContext({});
+
+const OuterElementType = React.forwardRef((props, ref) => {
+ const outerProps = React.useContext(OuterElementContext);
+
+ return ;
+});
+
+export const VirtualizedListbox = React.forwardRef(function ListboxComponent(props, ref) {
+ const { children, ...other } = props;
+ const itemData = React.Children.toArray(children);
+ const itemCount = itemData.length;
+
+ return (
+
+
+
+ {(size) => (
+
+ {renderRow}
+
+ )}
+
+
+
+ );
+});
diff --git a/packages/desktop-dock/src/components/Search/index.ts b/packages/desktop-dock/src/components/Search/index.ts
index dd08be20..204b2cea 100644
--- a/packages/desktop-dock/src/components/Search/index.ts
+++ b/packages/desktop-dock/src/components/Search/index.ts
@@ -1,3 +1,3 @@
export * from "./Search";
+export * from "./SearchButton";
export { default as SearchHelp } from "./SearchHelp";
-export * from "./SearchResults";
diff --git a/packages/desktop-dock/src/stores/applications/iApplication.ts b/packages/desktop-dock/src/stores/applications/iApplication.ts
index d5fc97e1..f346e06c 100644
--- a/packages/desktop-dock/src/stores/applications/iApplication.ts
+++ b/packages/desktop-dock/src/stores/applications/iApplication.ts
@@ -1,11 +1,11 @@
import { IConfiguration } from "@reactivemarkets/desktop-sdk";
export interface IApplication {
- readonly category: string;
+ readonly category?: string;
readonly configuration: IConfiguration;
readonly description?: string;
+ readonly display?: string;
readonly icon?: string;
readonly key: string;
- readonly launch: () => Promise;
readonly name: string;
}
diff --git a/packages/desktop-dock/src/stores/applications/iApplicationsStore.ts b/packages/desktop-dock/src/stores/applications/iApplicationsStore.ts
index 3ae78a61..b1ddb966 100644
--- a/packages/desktop-dock/src/stores/applications/iApplicationsStore.ts
+++ b/packages/desktop-dock/src/stores/applications/iApplicationsStore.ts
@@ -1,9 +1,12 @@
+import { IConfiguration } from "@reactivemarkets/desktop-sdk";
import { IApplication } from "./iApplication";
export interface IApplicationsStore {
readonly applications: readonly IApplication[];
+ readonly categories: readonly string[];
load(): void;
+ launch(application: IApplication): Promise;
remove(application: IApplication): Promise;
}
diff --git a/packages/desktop-dock/src/stores/applications/iDockAnnotations.ts b/packages/desktop-dock/src/stores/applications/iDockAnnotations.ts
index b82635bd..ccf719df 100644
--- a/packages/desktop-dock/src/stores/applications/iDockAnnotations.ts
+++ b/packages/desktop-dock/src/stores/applications/iDockAnnotations.ts
@@ -1,5 +1,6 @@
export interface IDockAnnotations {
readonly category?: string;
readonly excludeFromSearch?: boolean;
+ readonly display?: string;
readonly icon?: string;
}
diff --git a/packages/desktop-dock/src/stores/applications/observableApplicationsStore.ts b/packages/desktop-dock/src/stores/applications/observableApplicationsStore.ts
index f45898d8..1dfb115e 100644
--- a/packages/desktop-dock/src/stores/applications/observableApplicationsStore.ts
+++ b/packages/desktop-dock/src/stores/applications/observableApplicationsStore.ts
@@ -1,13 +1,6 @@
-import {
- desktop,
- launcher,
- registry,
- IConfiguration,
- WellKnownNamespace,
- WellKnownConfigurationKind,
-} from "@reactivemarkets/desktop-sdk";
+import { desktop, launcher, registry, IConfiguration, WellKnownConfigurationKind } from "@reactivemarkets/desktop-sdk";
import { from } from "ix/iterable";
-import { orderBy, thenBy } from "ix/iterable/operators";
+import { distinct, filter, map, orderBy, thenBy } from "ix/iterable/operators";
import { observable, action, computed } from "mobx";
import { IApplicationsStore } from "./iApplicationsStore";
import { IApplication } from "./iApplication";
@@ -28,9 +21,41 @@ export class ObservableApplicationsStore implements IApplicationsStore {
return Array.from(sorted);
}
+ @computed
+ public get categories() {
+ const values = this.applicationMap.values();
+
+ const categories = from(values).pipe(
+ map((item) => item.category),
+ filter((item) => item !== undefined),
+ distinct(),
+ map((item) => item!),
+ orderBy((item) => item),
+ );
+
+ return Array.from(categories);
+ }
+
public async load() {
try {
if (!desktop.isHostedInDesktop) {
+ for (let index = 0; index < 100; index++) {
+ this.addApplication({
+ kind: WellKnownConfigurationKind.Application,
+ metadata: {
+ name: "test" + index,
+ description: "Webpage",
+ annotations: {
+ "@reactivemarkets/desktop-dock": {
+ category: "web" + (index % 10),
+ display: "Reactive Markets | The Professional Markets Platform" + index,
+ },
+ },
+ },
+ spec: {},
+ });
+ }
+
return;
}
@@ -47,6 +72,12 @@ export class ObservableApplicationsStore implements IApplicationsStore {
}
}
+ public launch({ configuration }: IApplication) {
+ console.info("Launching application", configuration);
+
+ return launcher.launch(configuration);
+ }
+
public async remove({ configuration }: IApplication) {
await registry.unregister(configuration);
@@ -63,7 +94,8 @@ export class ObservableApplicationsStore implements IApplicationsStore {
}
const { annotations, description, name, namespace } = metadata;
- let category = namespace ?? WellKnownNamespace.default;
+ let category = namespace;
+ let display = undefined;
let icon = undefined;
if (annotations !== undefined) {
const dockAnnotations = annotations["@reactivemarkets/desktop-dock"] as IDockAnnotations | undefined;
@@ -73,6 +105,9 @@ export class ObservableApplicationsStore implements IApplicationsStore {
if (dockAnnotations?.category !== undefined) {
category = dockAnnotations.category;
}
+ if (dockAnnotations?.display !== undefined) {
+ display = dockAnnotations?.display;
+ }
if (dockAnnotations?.icon !== undefined) {
icon = dockAnnotations?.icon;
}
@@ -86,14 +121,10 @@ export class ObservableApplicationsStore implements IApplicationsStore {
category,
configuration,
description,
+ display,
icon,
key,
name,
- launch: () => {
- console.info("Launching application", configuration);
-
- return launcher.launch(configuration);
- },
});
};
diff --git a/packages/desktop-dock/src/stores/filter/iFilterStore.ts b/packages/desktop-dock/src/stores/filter/iFilterStore.ts
new file mode 100644
index 00000000..c3b3c4f9
--- /dev/null
+++ b/packages/desktop-dock/src/stores/filter/iFilterStore.ts
@@ -0,0 +1,6 @@
+export interface IFilterStore {
+ readonly filter?: string;
+
+ clear(): void;
+ update(filter: string): void;
+}
diff --git a/packages/desktop-dock/src/stores/filter/index.ts b/packages/desktop-dock/src/stores/filter/index.ts
new file mode 100644
index 00000000..fcb4177c
--- /dev/null
+++ b/packages/desktop-dock/src/stores/filter/index.ts
@@ -0,0 +1,2 @@
+export * from "./iFilterStore";
+export * from "./observableFIlterStore";
diff --git a/packages/desktop-dock/src/stores/filter/observableFIlterStore.ts b/packages/desktop-dock/src/stores/filter/observableFIlterStore.ts
new file mode 100644
index 00000000..5490fff4
--- /dev/null
+++ b/packages/desktop-dock/src/stores/filter/observableFIlterStore.ts
@@ -0,0 +1,16 @@
+import { action, observable } from "mobx";
+import { IFilterStore } from "./iFilterStore";
+
+export class ObservableFilterStore implements IFilterStore {
+ @observable
+ public filter?: string;
+
+ public clear() {
+ this.filter = undefined;
+ }
+
+ @action
+ public update(filter?: string) {
+ this.filter = filter;
+ }
+}
diff --git a/packages/desktop-dock/src/stores/index.ts b/packages/desktop-dock/src/stores/index.ts
index 7d2c14d5..a188ee19 100644
--- a/packages/desktop-dock/src/stores/index.ts
+++ b/packages/desktop-dock/src/stores/index.ts
@@ -1,4 +1,7 @@
export * from "./applications";
+export * from "./filter";
export * from "./focus";
export * from "./resizer";
export * from "./search";
+export * from "./tabs";
+export * from "./themes";
diff --git a/packages/desktop-dock/src/stores/search/iSearchProvider.ts b/packages/desktop-dock/src/stores/search/iSearchProvider.ts
deleted file mode 100644
index 413c57f5..00000000
--- a/packages/desktop-dock/src/stores/search/iSearchProvider.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { ISearchResult } from "./iSearchResult";
-
-export interface ISearchProvider {
- search(searchTerm?: string): Promise;
-}
diff --git a/packages/desktop-dock/src/stores/search/iSearchResult.ts b/packages/desktop-dock/src/stores/search/iSearchResult.ts
deleted file mode 100644
index e91c860f..00000000
--- a/packages/desktop-dock/src/stores/search/iSearchResult.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { IApplication } from "../applications";
-
-export interface ISearchResult {
- readonly item: IApplication;
- readonly provider: string;
- readonly score?: number;
-}
diff --git a/packages/desktop-dock/src/stores/search/iSearchStore.ts b/packages/desktop-dock/src/stores/search/iSearchStore.ts
index c12eca5a..3e8a1e47 100644
--- a/packages/desktop-dock/src/stores/search/iSearchStore.ts
+++ b/packages/desktop-dock/src/stores/search/iSearchStore.ts
@@ -1,13 +1,6 @@
-import { ISearchResult } from "./iSearchResult";
-
export interface ISearchStore {
- readonly error?: string;
-
- readonly results: readonly ISearchResult[];
-
- readonly searchTerm: string;
+ readonly term?: string;
clear(): void;
-
- search(searchTerm?: string): void;
+ update(term: string): void;
}
diff --git a/packages/desktop-dock/src/stores/search/index.ts b/packages/desktop-dock/src/stores/search/index.ts
index e79f319f..ba6fa89d 100644
--- a/packages/desktop-dock/src/stores/search/index.ts
+++ b/packages/desktop-dock/src/stores/search/index.ts
@@ -1,4 +1,2 @@
-export * from "./iSearchResult";
export * from "./iSearchStore";
export * from "./observableSearchStore";
-export * from "./providers";
diff --git a/packages/desktop-dock/src/stores/search/observableSearchStore.ts b/packages/desktop-dock/src/stores/search/observableSearchStore.ts
index 01bcb886..5b4288d1 100644
--- a/packages/desktop-dock/src/stores/search/observableSearchStore.ts
+++ b/packages/desktop-dock/src/stores/search/observableSearchStore.ts
@@ -1,47 +1,17 @@
+import { action, observable } from "mobx";
import { ISearchStore } from "./iSearchStore";
-import { ISearchResult } from "./iSearchResult";
-import { observable, action, runInAction } from "mobx";
-import { ISearchProvider } from "./iSearchProvider";
export class ObservableSearchStore implements ISearchStore {
- public results = observable.array([], { deep: false });
-
- @observable
- public error?: string;
-
@observable
- public searchTerm = "";
-
- private readonly searchProvider: ISearchProvider;
-
- public constructor(searchProvider: ISearchProvider) {
- this.searchProvider = searchProvider;
- }
+ public term?: string;
@action
public clear() {
- this.searchTerm = "";
+ this.term = undefined;
}
@action
- public search(searchTerm = "") {
- console.log(`Searching for: "${searchTerm}"`);
-
- this.searchTerm = searchTerm;
-
- this.searchProvider
- .search(searchTerm)
- .then((results) => {
- runInAction(() => {
- this.results.replace(results);
- console.log(`Replacing results with: ${results.length}`);
- });
- })
- .catch((error) => {
- runInAction(() => {
- this.error = `${error}`;
- console.error("Error searching for items", error);
- });
- });
+ public update(term: string) {
+ this.term = term;
}
}
diff --git a/packages/desktop-dock/src/stores/search/providers/applicationsSearchProvider.ts b/packages/desktop-dock/src/stores/search/providers/applicationsSearchProvider.ts
deleted file mode 100644
index 537096dd..00000000
--- a/packages/desktop-dock/src/stores/search/providers/applicationsSearchProvider.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import Fuse from "fuse.js";
-import { ISearchProvider } from "../iSearchProvider";
-import { IApplicationsStore, IApplication } from "../../applications";
-
-export class ApplicationsSearchProvider implements ISearchProvider {
- private readonly applicationsStore: IApplicationsStore;
- private readonly provider = "application";
-
- public constructor(applicationsStore: IApplicationsStore) {
- this.applicationsStore = applicationsStore;
- }
-
- public search(searchTerm?: string) {
- const { applications } = this.applicationsStore;
-
- if (searchTerm === undefined || searchTerm === "") {
- const results = applications.map((application) => {
- return this.toSearchResult(application);
- });
-
- return Promise.resolve(results);
- }
-
- const fuse = new Fuse(applications, {
- ignoreLocation: true,
- includeScore: true,
- shouldSort: false,
- useExtendedSearch: true,
- keys: ["name", "description", "category"],
- });
-
- const results = fuse.search(searchTerm);
-
- const providerResults = results.map((r) => ({
- ...r,
- provider: this.provider,
- }));
-
- return Promise.resolve(providerResults);
- }
-
- private readonly toSearchResult = (application: IApplication) => {
- return {
- item: application,
- provider: this.provider,
- score: 0,
- };
- };
-}
diff --git a/packages/desktop-dock/src/stores/search/providers/index.ts b/packages/desktop-dock/src/stores/search/providers/index.ts
deleted file mode 100644
index 35a857c9..00000000
--- a/packages/desktop-dock/src/stores/search/providers/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { TermCleaningSearchProvider } from "./termCleaningSearchProvider";
-import { OrderedSearchProvider } from "./orderedSearchProvider";
-import { ApplicationsSearchProvider } from "./applicationsSearchProvider";
-import { IApplicationsStore } from "../../applications";
-import { MaxScoreSearchProvider } from "./maxScoreSearchProvider";
-
-export const createSearchProvider = (applicationsStore: IApplicationsStore) => {
- const applications = new ApplicationsSearchProvider(applicationsStore);
-
- const maxScore = new MaxScoreSearchProvider(applications, 0.6);
-
- const ordered = new OrderedSearchProvider(maxScore);
-
- return new TermCleaningSearchProvider(ordered);
-};
diff --git a/packages/desktop-dock/src/stores/search/providers/maxScoreSearchProvider.ts b/packages/desktop-dock/src/stores/search/providers/maxScoreSearchProvider.ts
deleted file mode 100644
index 271b9ed0..00000000
--- a/packages/desktop-dock/src/stores/search/providers/maxScoreSearchProvider.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ISearchProvider } from "../iSearchProvider";
-
-export class MaxScoreSearchProvider implements ISearchProvider {
- private readonly maxScore: number;
- private readonly searchProvider: ISearchProvider;
-
- public constructor(searchProvider: ISearchProvider, maxScore: number) {
- this.searchProvider = searchProvider;
- this.maxScore = maxScore;
- }
-
- public async search(searchTerm?: string) {
- const results = await this.searchProvider.search(searchTerm);
-
- return results.filter(({ score }) => {
- if (score === undefined) {
- return false;
- }
-
- return score < this.maxScore;
- });
- }
-}
diff --git a/packages/desktop-dock/src/stores/search/providers/orderedSearchProvider.ts b/packages/desktop-dock/src/stores/search/providers/orderedSearchProvider.ts
deleted file mode 100644
index 0635e4db..00000000
--- a/packages/desktop-dock/src/stores/search/providers/orderedSearchProvider.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { ISearchProvider } from "../iSearchProvider";
-
-export class OrderedSearchProvider implements ISearchProvider {
- private readonly searchProvider: ISearchProvider;
-
- public constructor(searchProvider: ISearchProvider) {
- this.searchProvider = searchProvider;
- }
-
- public async search(searchTerm?: string) {
- const results = await this.searchProvider.search(searchTerm);
-
- return results.sort((a, b) => {
- if (a.score === undefined) {
- return 1;
- }
-
- if (b.score === undefined) {
- return -1;
- }
-
- if (a.score < b.score) {
- return -1;
- }
-
- if (a.score > b.score) {
- return 1;
- }
-
- return 0;
- });
- }
-}
diff --git a/packages/desktop-dock/src/stores/search/providers/termCleaningSearchProvider.ts b/packages/desktop-dock/src/stores/search/providers/termCleaningSearchProvider.ts
deleted file mode 100644
index 41a30ad7..00000000
--- a/packages/desktop-dock/src/stores/search/providers/termCleaningSearchProvider.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { ISearchProvider } from "../iSearchProvider";
-
-export class TermCleaningSearchProvider implements ISearchProvider {
- private readonly searchProvider: ISearchProvider;
-
- public constructor(searchProvider: ISearchProvider) {
- this.searchProvider = searchProvider;
- }
-
- public search(searchTerm?: string) {
- if (searchTerm === undefined) {
- return this.searchProvider.search();
- }
-
- const cleanedSearchTerm = searchTerm.trim();
-
- return this.searchProvider.search(cleanedSearchTerm);
- }
-}
diff --git a/packages/desktop-dock/src/stores/tabs/iTabStore.ts b/packages/desktop-dock/src/stores/tabs/iTabStore.ts
new file mode 100644
index 00000000..f3d0a8bf
--- /dev/null
+++ b/packages/desktop-dock/src/stores/tabs/iTabStore.ts
@@ -0,0 +1,10 @@
+import { Observable } from "rxjs";
+
+export type Action = "next" | "previous";
+
+export interface ITabStore {
+ action: Observable;
+
+ next(): void;
+ previous(): void;
+}
diff --git a/packages/desktop-dock/src/stores/tabs/index.ts b/packages/desktop-dock/src/stores/tabs/index.ts
new file mode 100644
index 00000000..73590663
--- /dev/null
+++ b/packages/desktop-dock/src/stores/tabs/index.ts
@@ -0,0 +1,2 @@
+export * from "./iTabStore";
+export * from "./observableTabStore";
diff --git a/packages/desktop-dock/src/stores/tabs/observableTabStore.ts b/packages/desktop-dock/src/stores/tabs/observableTabStore.ts
new file mode 100644
index 00000000..33c8b5e5
--- /dev/null
+++ b/packages/desktop-dock/src/stores/tabs/observableTabStore.ts
@@ -0,0 +1,14 @@
+import { Subject } from "rxjs";
+import { Action, ITabStore } from "./iTabStore";
+
+export class ObservableTabStore implements ITabStore {
+ public readonly action = new Subject();
+
+ public next() {
+ this.action.next("next");
+ }
+
+ public previous() {
+ this.action.next("previous");
+ }
+}
diff --git a/packages/desktop-dock/src/stores/themes/darkTheme.ts b/packages/desktop-dock/src/stores/themes/darkTheme.ts
new file mode 100644
index 00000000..02760476
--- /dev/null
+++ b/packages/desktop-dock/src/stores/themes/darkTheme.ts
@@ -0,0 +1,114 @@
+import { ThemeOptions } from "@material-ui/core";
+
+export const darkTheme: ThemeOptions = {
+ overrides: {
+ MuiCssBaseline: {
+ "@global": {
+ ".drag": {
+ "-webkit-app-region": "drag",
+ },
+ ".no-drag": {
+ "-webkit-app-region": "no-drag",
+ },
+ "::-webkit-scrollbar": {
+ height: 12,
+ width: 12,
+ },
+ "::-webkit-scrollbar-corner": {
+ background: "transparent",
+ },
+ "::-webkit-scrollbar-thumb": {
+ background: "rgba(255, 255, 255, 0.12)",
+ boxShadow: "inset 1px 1px 2px rgba(0, 0, 0, 0.2)",
+ },
+ "::-webkit-scrollbar-thumb:active": {
+ background: "#fff",
+ boxShadow: "inset 1px 1px 2px rgba(0, 0, 0, 0.3)",
+ },
+ "::-webkit-scrollbar-thumb:hover": {
+ background: "rgba(255, 255, 255, 0.08)",
+ },
+ "::-webkit-scrollbar-track": {
+ boxShadow: "inset 1px 1px 2px rgba(0, 0, 0, 0.1)",
+ },
+ "::selection": {
+ background: "#FEBF00",
+ color: "#000000",
+ },
+ "#app": {
+ display: "flex",
+ flex: 1,
+ },
+ body: {
+ display: "flex",
+ height: "100vh",
+ overflow: "hidden",
+ userSelect: "none",
+ },
+ input: {
+ caretColor: "#FEBF00",
+ },
+ },
+ },
+ MuiListItem: {
+ dense: {
+ paddingTop: 2,
+ paddingBottom: 2,
+ },
+ gutters: {
+ paddingLeft: 16,
+ },
+ },
+ MuiListItemIcon: {
+ root: {
+ minWidth: 40,
+ },
+ },
+ MuiListItemText: {
+ inset: {
+ paddingLeft: 42,
+ },
+ },
+ MuiTab: {
+ root: {
+ "@media (min-width: 600px)": {
+ minWidth: 48,
+ },
+ fontSize: 13,
+ minHeight: 40,
+ minWidth: 48,
+ padding: "4px 8px",
+ textTransform: "capitalize",
+ },
+ },
+ MuiTabs: {
+ root: {
+ minHeight: 40,
+ },
+ scrollButtons: {
+ width: 16,
+ },
+ },
+ },
+ palette: {
+ primary: {
+ main: "#4A90E2",
+ },
+ secondary: {
+ main: "#4A90E2",
+ },
+ type: "dark",
+ },
+ props: {
+ MuiTooltip: {
+ arrow: true,
+ enterDelay: 500,
+ },
+ },
+ typography: {
+ body2: {
+ fontSize: 13,
+ fontWeight: 500,
+ },
+ },
+};
diff --git a/packages/desktop-dock/src/stores/themes/iThemeStore.ts b/packages/desktop-dock/src/stores/themes/iThemeStore.ts
new file mode 100644
index 00000000..8c3e70ae
--- /dev/null
+++ b/packages/desktop-dock/src/stores/themes/iThemeStore.ts
@@ -0,0 +1,5 @@
+import { ThemeOptions } from "@material-ui/core";
+
+export interface IThemeStore {
+ readonly current: ThemeOptions;
+}
diff --git a/packages/desktop-dock/src/stores/themes/index.ts b/packages/desktop-dock/src/stores/themes/index.ts
new file mode 100644
index 00000000..0cd8724e
--- /dev/null
+++ b/packages/desktop-dock/src/stores/themes/index.ts
@@ -0,0 +1,2 @@
+export * from "./iThemeStore";
+export * from "./observableThemeStore";
diff --git a/packages/desktop-dock/src/stores/themes/observableThemeStore.ts b/packages/desktop-dock/src/stores/themes/observableThemeStore.ts
new file mode 100644
index 00000000..82490d38
--- /dev/null
+++ b/packages/desktop-dock/src/stores/themes/observableThemeStore.ts
@@ -0,0 +1,8 @@
+import { observable } from "mobx";
+import { darkTheme } from "./darkTheme";
+import { IThemeStore } from "./iThemeStore";
+
+export class ObservableThemeStore implements IThemeStore {
+ @observable
+ public current = darkTheme;
+}
diff --git a/packages/desktop-dock/webpack.common.js b/packages/desktop-dock/webpack.common.js
index 85bdd173..e1e9fdce 100644
--- a/packages/desktop-dock/webpack.common.js
+++ b/packages/desktop-dock/webpack.common.js
@@ -106,7 +106,7 @@ const config = {
"default-src": "'none'",
"base-uri": "'none'",
"connect-src": "'self'",
- "img-src": "'self'",
+ "img-src": ["'self'", "https://*"],
"manifest-src": "'self'",
"font-src": ["'self'", "data:"],
"form-action": "'none'",