diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 5f94e62f..545c1a96 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -18,6 +18,7 @@ rules: "@typescript-eslint/explicit-module-boundary-types": off "@typescript-eslint/interface-name-prefix": off "@typescript-eslint/no-non-null-assertion": off + "react/display-name": off "react/prop-types": off "require-await": "error" "no-return-await": "error" diff --git a/.vscode/launch.json b/.vscode/launch.json index 0cab62e4..7b32b933 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -94,7 +94,6 @@ "type": "node", "request": "launch", "cwd": "${workspaceFolder}", - "preLaunchTask": "npm: build:dev:core", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" @@ -118,7 +117,6 @@ "type": "node", "request": "launch", "cwd": "${workspaceFolder}", - "preLaunchTask": "npm: build:dev:core", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" diff --git a/package-lock.json b/package-lock.json index e230263c..2acb401e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3974,6 +3974,18 @@ "react-transition-group": "^4.4.0" } }, + "@material-ui/lab": { + "version": "4.0.0-alpha.57", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.57.tgz", + "integrity": "sha512-qo/IuIQOmEKtzmRD2E4Aa6DB4A87kmY6h0uYhjUmrrgmEAgbbw9etXpWPVXuRK6AGIQCjFzV6WO2i21m1R4FCw==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, "@material-ui/styles": { "version": "4.11.2", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.2.tgz", @@ -4294,9 +4306,10 @@ "version": "file:packages/desktop-dock", "requires": { "@material-ui/core": "^4.11.2", + "@material-ui/lab": "^4.0.0-alpha.57", "@reactivemarkets/desktop-sdk": "file:packages/desktop-sdk", - "fuse.js": "^6.4.3", "ix": "^4.0.0", + "match-sorter": "^6.0.2", "mdi-material-ui": "^6.20.0", "mobx": "^5.15.7", "mobx-react": "^6.3.0", @@ -14280,11 +14293,6 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, - "fuse.js": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.4.3.tgz", - "integrity": "sha512-JNgngolukIrqwayWnvy6NLH63hmwKPhm63o0uyBg51jPD0j09IvAzlV1rTXfAsgxpghI7khAo6Mv+EmvjDWXig==" - }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -18420,6 +18428,15 @@ "integrity": "sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==", "dev": true }, + "match-sorter": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.0.2.tgz", + "integrity": "sha512-SDRLNlWof9GnAUEyhKP0O5525MMGXUGt+ep4MrrqQ2StAh3zjvICVZseiwg7Zijn3GazpJDiwuRr/mFDHd92NQ==", + "requires": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, "matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -24259,6 +24276,11 @@ "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", "dev": true }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=" + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", diff --git a/packages/desktop-dock/package.json b/packages/desktop-dock/package.json index bbab4df1..3aab5a8b 100644 --- a/packages/desktop-dock/package.json +++ b/packages/desktop-dock/package.json @@ -32,9 +32,10 @@ "license": "Apache-2.0", "dependencies": { "@material-ui/core": "^4.11.2", + "@material-ui/lab": "^4.0.0-alpha.57", "@reactivemarkets/desktop-sdk": "file:../desktop-sdk", - "fuse.js": "^6.4.3", "ix": "^4.0.0", + "match-sorter": "^6.0.2", "mdi-material-ui": "^6.20.0", "mobx": "^5.15.7", "mobx-react": "^6.3.0", diff --git a/packages/desktop-dock/src/components/App/App.tsx b/packages/desktop-dock/src/components/App/App.tsx index cad72cff..c38395e5 100644 --- a/packages/desktop-dock/src/components/App/App.tsx +++ b/packages/desktop-dock/src/components/App/App.tsx @@ -1,24 +1,33 @@ import * as React from "react"; -import AppShell from "./AppShell"; +import { AppShell } from "./AppShell"; import { AppTheme } from "./AppTheme"; import { ApplicationsStoreProvider } from "./ApplicationsStoreProvider"; import { FocusStoreProvider } from "./FocusStoreProvider"; import { ResizerStoreProvider } from "./ResizerStoreProvider"; +import { FilterStoreProvider } from "./FilterStoreProvider"; import { SearchStoreProvider } from "./SearchStoreProvider"; +import { ThemeStoreProvider } from "./ThemeStoreProvider"; +import { TabStoreProvider } from "./TabStoreProvider"; export class App extends React.PureComponent { public render() { return ( - - - - - - - - - + + + + + + + + + + + + + + + ); } diff --git a/packages/desktop-dock/src/components/App/AppShell.tsx b/packages/desktop-dock/src/components/App/AppShell.tsx index 4746c41a..f746571c 100644 --- a/packages/desktop-dock/src/components/App/AppShell.tsx +++ b/packages/desktop-dock/src/components/App/AppShell.tsx @@ -1,33 +1,23 @@ -import { createStyles, Box, Grid, Theme, withStyles, WithStyles } from "@material-ui/core"; +import { Box } from "@material-ui/core"; import * as React from "react"; -import { Search, SearchHelp, SearchResults } from "../Search"; +import { Search, SearchButton, SearchHelp } from "../Search"; import { DragHandle, PowerButton } from "../System"; -const styles = (theme: Theme) => - createStyles({ - padding: { - padding: theme.spacing(1, 1, 1, 0), - }, - }); - -class AppShell extends React.PureComponent> { +export class AppShell extends React.PureComponent { public render() { - const { classes } = this.props; - return ( - - - - - - - - + + + + + } + endAdornment={} + /> ); } } - -export default withStyles(styles)(AppShell); diff --git a/packages/desktop-dock/src/components/App/AppTheme.tsx b/packages/desktop-dock/src/components/App/AppTheme.tsx index 5425ee83..c69b4d43 100644 --- a/packages/desktop-dock/src/components/App/AppTheme.tsx +++ b/packages/desktop-dock/src/components/App/AppTheme.tsx @@ -6,20 +6,27 @@ import { CssBaseline, } from "@material-ui/core"; import { computed } from "mobx"; -import { observer } from "mobx-react"; +import { inject, observer } from "mobx-react"; import * as React from "react"; -import { defaultTheme } from "./defaultTheme"; +import { IThemeStore } from "../../stores"; const generateClassName = createGenerateClassName({ disableGlobal: true, productionPrefix: "d", }); +interface IAppThemeProps { + readonly themeStore?: IThemeStore; +} + +@inject("themeStore") @observer -export class AppTheme extends React.Component { +export class AppTheme extends React.Component { @computed private get currentThemeOptions() { - return createMuiTheme(defaultTheme); + const { current } = this.props.themeStore!; + + return createMuiTheme(current); } public render() { diff --git a/packages/desktop-dock/src/components/App/FilterStoreProvider.tsx b/packages/desktop-dock/src/components/App/FilterStoreProvider.tsx new file mode 100644 index 00000000..8a0d95dd --- /dev/null +++ b/packages/desktop-dock/src/components/App/FilterStoreProvider.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { Provider } from "mobx-react"; +import { ObservableFilterStore } from "../../stores"; + +export class FilterStoreProvider extends React.PureComponent { + private readonly filterStore = new ObservableFilterStore(); + + public render() { + return {this.props.children}; + } +} diff --git a/packages/desktop-dock/src/components/App/SearchStoreProvider.tsx b/packages/desktop-dock/src/components/App/SearchStoreProvider.tsx index 05d2e96c..1a7b0db8 100644 --- a/packages/desktop-dock/src/components/App/SearchStoreProvider.tsx +++ b/packages/desktop-dock/src/components/App/SearchStoreProvider.tsx @@ -1,22 +1,9 @@ import * as React from "react"; -import { Provider, inject } from "mobx-react"; -import { ISearchStore, IApplicationsStore, ObservableSearchStore, createSearchProvider } from "../../stores"; +import { Provider } from "mobx-react"; +import { ObservableSearchStore } from "../../stores"; -interface ISearchStoreProviderProps { - readonly applicationsStore?: IApplicationsStore; -} - -@inject("applicationsStore") -export class SearchStoreProvider extends React.PureComponent { - private readonly searchStore: ISearchStore; - - public constructor(props: ISearchStoreProviderProps) { - super(props); - - const searchProvider = createSearchProvider(props.applicationsStore!); - - this.searchStore = new ObservableSearchStore(searchProvider); - } +export class SearchStoreProvider extends React.PureComponent { + private readonly searchStore = new ObservableSearchStore(); public render() { return {this.props.children}; diff --git a/packages/desktop-dock/src/components/App/TabStoreProvider.tsx b/packages/desktop-dock/src/components/App/TabStoreProvider.tsx new file mode 100644 index 00000000..091725f6 --- /dev/null +++ b/packages/desktop-dock/src/components/App/TabStoreProvider.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { Provider } from "mobx-react"; +import { ObservableTabStore } from "../../stores"; + +export class TabStoreProvider extends React.PureComponent { + private readonly tabStore = new ObservableTabStore(); + + public render() { + return {this.props.children}; + } +} diff --git a/packages/desktop-dock/src/components/App/ThemeStoreProvider.tsx b/packages/desktop-dock/src/components/App/ThemeStoreProvider.tsx new file mode 100644 index 00000000..41c42f7e --- /dev/null +++ b/packages/desktop-dock/src/components/App/ThemeStoreProvider.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { Provider } from "mobx-react"; +import { ObservableThemeStore } from "../../stores"; + +export class ThemeStoreProvider extends React.PureComponent { + private readonly themeStore = new ObservableThemeStore(); + + public render() { + return {this.props.children}; + } +} diff --git a/packages/desktop-dock/src/components/App/defaultTheme.ts b/packages/desktop-dock/src/components/App/defaultTheme.ts index c2d219f5..8a63f43d 100644 --- a/packages/desktop-dock/src/components/App/defaultTheme.ts +++ b/packages/desktop-dock/src/components/App/defaultTheme.ts @@ -50,12 +50,63 @@ export const defaultTheme: ThemeOptions = { }, }, }, + 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, + paddingLeft: 16, + }, + }, + }, + palette: { + primary: { + main: "#4A90E2", + }, + secondary: { + main: "#4A90E2", + }, + type: "dark", }, - palette: { type: "dark" }, props: { MuiTooltip: { arrow: true, enterDelay: 500, }, }, + typography: { + body2: { + fontSize: 13, + fontWeight: 500, + }, + }, }; diff --git a/packages/desktop-dock/src/components/Search/CategoryTabs.tsx b/packages/desktop-dock/src/components/Search/CategoryTabs.tsx new file mode 100644 index 00000000..f9e5581e --- /dev/null +++ b/packages/desktop-dock/src/components/Search/CategoryTabs.tsx @@ -0,0 +1,96 @@ +import { createStyles, Tab, Tabs, withStyles, WithStyles } from "@material-ui/core"; +import { observer, inject } from "mobx-react"; +import * as React from "react"; +import { Subscription } from "rxjs"; +import { IApplicationsStore, IFilterStore, ITabStore } from "../../stores"; + +const styles = () => + createStyles({ + root: { + width: "100vw", + }, + }); + +interface ICategoryTabsProps extends WithStyles { + readonly applicationsStore?: IApplicationsStore; + readonly filterStore?: IFilterStore; + readonly tabStore?: ITabStore; +} + +@inject("applicationsStore") +@inject("filterStore") +@inject("tabStore") +@observer +class CategoryTabs extends React.Component { + 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 (
- + icon
); } 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'",