From 6624287626f846854e7edbf2b2485765d868636a Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 19 Nov 2020 19:49:00 +0200 Subject: [PATCH] Option to install an extension from filesystem/url #1227 -- part 1 (UI) Signed-off-by: Roman --- .../components/+extensions/extensions.scss | 16 +++- .../components/+extensions/extensions.tsx | 82 +++++++++++++++++-- .../copy-to-click/copy-to-click.tsx | 58 +++++++++++++ .../components/copy-to-click/index.ts | 1 + src/renderer/components/icon/icon.tsx | 2 +- src/renderer/components/input/input.scss | 6 ++ src/renderer/components/input/input.tsx | 34 +++++--- src/renderer/components/tooltip/tooltip.scss | 13 ++- src/renderer/utils/copyToClipboard.ts | 18 ++-- 9 files changed, 200 insertions(+), 30 deletions(-) create mode 100644 src/renderer/components/copy-to-click/copy-to-click.tsx create mode 100644 src/renderer/components/copy-to-click/index.ts diff --git a/src/renderer/components/+extensions/extensions.scss b/src/renderer/components/+extensions/extensions.scss index 63778d37e9..f69470fc73 100644 --- a/src/renderer/components/+extensions/extensions.scss +++ b/src/renderer/components/+extensions/extensions.scss @@ -2,7 +2,7 @@ --width: 100%; --max-width: auto; - .extension-list { + .extensions-list { .extension { --flex-gap: $padding / 3; padding: $padding $padding * 2; @@ -15,6 +15,20 @@ } } + .extensions-info { + .flex.gaps { + --flex-gap: #{$padding}; + } + .install-extension { + code { + &:hover { + color: $textColorSecondary; + } + cursor: pointer; + } + } + } + .extensions-path { word-break: break-all; } diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 875861a8dd..14a22f7061 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -1,5 +1,5 @@ import "./extensions.scss"; -import { shell } from "electron"; +import { remote, shell } from "electron"; import React from "react"; import { computed, observable } from "mobx"; import { observer } from "mobx-react"; @@ -7,15 +7,18 @@ import { t, Trans } from "@lingui/macro"; import { _i18n } from "../../i18n"; import { Button } from "../button"; import { WizardLayout } from "../layout/wizard-layout"; -import { Input } from "../input"; +import { Input, InputValidators } from "../input"; import { Icon } from "../icon"; import { PageLayout } from "../layout/page-layout"; +import { CopyToClick } from "../copy-to-click/copy-to-click"; import { extensionLoader } from "../../../extensions/extension-loader"; import { extensionManager } from "../../../extensions/extension-manager"; @observer export class Extensions extends React.Component { + @observable.ref input: Input; @observable search = ""; + @observable downloadUrl = ""; @computed get extensions() { const searchText = this.search.toLowerCase(); @@ -32,16 +35,43 @@ export class Extensions extends React.Component { return extensionManager.localFolderPath; } + selectPackedExtensionsDialog = async () => { + const { dialog, BrowserWindow, app } = remote; + const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), { + defaultPath: app.getPath("downloads"), + properties: ["openFile", "multiSelections"], + message: _i18n._(t`Select extensions to add to Lens (*.tgz`), + buttonLabel: _i18n._(t`Use configuration`), + filters: [ + { name: "tarball", extensions: [".tgz", ".tar.gz"] } + ] + }); + if (!canceled && filePaths.length) { + this.installFromLocalPath(filePaths); + } + } + + installFromUrl = () => { + if (!this.downloadUrl) { + this.input?.focus(); + return; + } + + } + + installFromLocalPath = (filePaths: string[]) => { + } + renderInfo() { return ( -
+

Lens Extension API

The Extensions API in Lens allows users to customize and enhance the Lens experience by creating their own menus or page content that is extended from the existing pages. Many of the core features of Lens are built as extensions and use the same Extension API.
- Extensions loaded from: + Extensions installed and loaded from
{this.extensionsPath}
-
- Check out documentation to learn more +
+ Install extensions from local file-system or URL: +
+ this.downloadUrl = v} + ref={e => this.input = e} + /> + 0} + onClick={this.installFromUrl} + /> +
+
+
+ +

+ Check out documentation to learn more +

); @@ -104,7 +172,7 @@ export class Extensions extends React.Component { value={this.search} onChange={(value) => this.search = value} /> -
+
{this.renderExtensions()}
diff --git a/src/renderer/components/copy-to-click/copy-to-click.tsx b/src/renderer/components/copy-to-click/copy-to-click.tsx new file mode 100644 index 0000000000..ba11941e9f --- /dev/null +++ b/src/renderer/components/copy-to-click/copy-to-click.tsx @@ -0,0 +1,58 @@ +import React from "react" +import { findDOMNode } from "react-dom"; +import { autobind } from "../../../common/utils"; +import { Notifications } from "../notifications"; +import { copyToClipboard } from "../../utils/copyToClipboard"; +import logger from "../../../main/logger"; + +export interface CopyToClickProps { + resetSelection?: boolean + showNotification?: boolean + getNotificationMessage?(copiedText: string): React.ReactNode; +} + +export const defaultProps: Partial = { + getNotificationMessage(copiedText: string) { + return

Copied to clipboard: {copiedText}

+ } +} + +export class CopyToClick extends React.Component { + static defaultProps = defaultProps as object; + + get rootElem(): HTMLElement { + return findDOMNode(this) as HTMLElement; + } + + get rootReactElem(): React.ReactElement> { + return React.Children.only(this.props.children) as React.ReactElement; + } + + @autobind() + onClick(evt: React.MouseEvent) { + if (!this.rootElem || !this.rootElem.contains(evt.target as any)) { + return; + } + const { showNotification, resetSelection, getNotificationMessage } = this.props; + const { copiedText, copied } = copyToClipboard(this.rootElem, { resetSelection }); + if (copied && showNotification) { + Notifications.ok(getNotificationMessage(copiedText)); + } + if (this.rootReactElem.props.onClick) { + this.rootReactElem.props.onClick(evt); // pass event to content element as well when provided + } + } + + render() { + try { + const rootElem = this.rootReactElem; + return React.cloneElement(rootElem, { + ...(rootElem || {}).props, + onClick: this.onClick, + }) + } catch (err) { + logger.error(`Invalid usage components/CopyToClick usage. Children must contain root html element.`, { err: String(err) }) + return this.rootReactElem; + } + } +} \ No newline at end of file diff --git a/src/renderer/components/copy-to-click/index.ts b/src/renderer/components/copy-to-click/index.ts new file mode 100644 index 0000000000..c927dba518 --- /dev/null +++ b/src/renderer/components/copy-to-click/index.ts @@ -0,0 +1 @@ +export * from "./copy-to-click" diff --git a/src/renderer/components/icon/icon.tsx b/src/renderer/components/icon/icon.tsx index 8e99eb0095..24ff28b7c1 100644 --- a/src/renderer/components/icon/icon.tsx +++ b/src/renderer/components/icon/icon.tsx @@ -32,7 +32,7 @@ export class Icon extends React.PureComponent { get isInteractive() { const { interactive, onClick, href, link } = this.props; - return interactive || !!(onClick || href || link); + return interactive ?? !!(onClick || href || link); } @autobind() diff --git a/src/renderer/components/input/input.scss b/src/renderer/components/input/input.scss index 31f3c9c46c..39e38a0e10 100644 --- a/src/renderer/components/input/input.scss +++ b/src/renderer/components/input/input.scss @@ -107,3 +107,9 @@ } } } + +.Tooltip.InputTooltipError { + --bgc: #{$colorError}; + --color: white; + --border: 1px solid currentColor; +} diff --git a/src/renderer/components/input/input.tsx b/src/renderer/components/input/input.tsx index e61ecc3020..6b4667af83 100644 --- a/src/renderer/components/input/input.tsx +++ b/src/renderer/components/input/input.tsx @@ -1,7 +1,7 @@ import "./input.scss"; import React, { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react"; -import { autobind, cssNames, debouncePromise } from "../../utils"; +import { autobind, cssNames, debouncePromise, getRandId } from "../../utils"; import { Icon } from "../icon"; import * as Validators from "./input_validators"; import { InputValidator } from "./input_validators"; @@ -9,6 +9,7 @@ import isString from "lodash/isString"; import isFunction from "lodash/isFunction"; import isBoolean from "lodash/isBoolean"; import uniqueId from "lodash/uniqueId"; +import { Tooltip } from "../tooltip"; const { conditionalValidators, ...InputValidators } = Validators; export { InputValidators, InputValidator }; @@ -25,6 +26,7 @@ export type InputProps = Omit { render() { const { - multiLine, showValidationLine, validators, theme, maxRows, children, + multiLine, showValidationLine, validators, theme, maxRows, children, showErrorsAsTooltip, maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight, ...inputProps } = this.props; @@ -292,21 +294,31 @@ export class Input extends React.Component { ref: this.bindRef, spellCheck: "false", }); - + const tooltipId = showErrorsAsTooltip ? getRandId({ prefix: "input_tooltip_id" }) : undefined; + const showErrors = errors.length > 0 && !valid && dirty; + const errorsInfo = ( +
+ {errors.map((error, i) =>

{error}

)} +
+ ); return ( -
-