Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#688 New Autosuggest component for SelectorSelectorField #1059

Merged
merged 22 commits into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,014 changes: 1,014 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@maxim_mazurok/gapi.client.bigquery": "^2.0.20210701",
"@reduxjs/toolkit": "^1.6.0",
"@rjsf/core": "^3.0.0",
"@types/react-autosuggest": "^10.1.5",
"@uipath/robot": "^1.2.5",
"ace-builds": "^1.4.12",
"axios": "^0.21.1",
Expand Down Expand Up @@ -83,6 +84,7 @@
"psl": "^1.8.0",
"react": "^16.13.1",
"react-ace": "^9.4.1",
"react-autosuggest": "^10.1.0",
twschiller marked this conversation as resolved.
Show resolved Hide resolved
"react-beautiful-dnd": "^13.1.0",
"react-bootstrap": "^1.6.1",
"react-dom": "^16.13.1",
Expand Down Expand Up @@ -211,6 +213,7 @@
"ts-loader": "^9.2.4",
"type-fest": "^1.2.2",
"typescript": "^4.3.5",
"typescript-plugin-css-modules": "^3.4.0",
"webpack": "^5.46.0",
"webpack-build-notifier": "^2.3.0",
"webpack-bundle-analyzer": "^4.4.2",
Expand Down
19 changes: 19 additions & 0 deletions src/Globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (C) 2021 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

declare module "*.module.css";
Copy link
Contributor

@twschiller twschiller Aug 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BLoe Are these declarations missing something?

The documentation has more: https://github.com/mrmckeb/typescript-plugin-css-modules#custom-definitions

I think this should probably also help with the IDE autocomplete issue you were seeing?

declare module "*.module.css" {
  const classes: Record<string, string>;
  export default classes;
}

declare module "*.module.scss" {
  const classes: Record<string, string>;
  export default classes;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure what those extra parts were doing, and those lines only seemed to appear in some of the guides I looked at. I am not really seeing any difference when I add those lines and re-build though. They aren't related to the Sass warnings or anything.

declare module "*.module.scss";
15 changes: 2 additions & 13 deletions src/action.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,11 @@ body {
}
}

.action-panel-close-button {
padding: 4px;
margin: 4px 12px 4px 12px;
.action-panel-button {
color: #6562aa;

svg:last-child {
margin-left: -3px;
}

&:hover {
color: #4d4b8b;
background-color: #d1c2d7;
}
}

.ActionPanelToolbar {
.btn {
border-radius: 0;
}
}
35 changes: 17 additions & 18 deletions src/actionPanel/ActionPanelApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import ErrorBoundary from "@/components/ErrorBoundary";
import logo from "@img/logo.svg";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faPuzzlePiece,
faCog,
faSpinner,
faChevronRight,
faAngleDoubleRight
} from "@fortawesome/free-solid-svg-icons";
import { getStore } from "@/actionPanel/native";
import {
Expand Down Expand Up @@ -138,18 +138,18 @@ const ActionPanelApp: React.FunctionComponent = () => {
<PersistGate loading={<GridLoader />} persistor={persistor}>
<ToastProvider>
<div className="d-flex flex-column" style={{ height: "100vh" }}>
<div className="d-flex mb-2" style={{ flex: "none" }}>
<div className="d-flex flex-row mb-2 p-2 justify-content-between align-content-center">
<Button
className="action-panel-close-button"
className="action-panel-button"
onClick={closeSidebar}
size="sm"
variant="link"
>
<FontAwesomeIcon icon={faChevronRight} />
<FontAwesomeIcon icon={faChevronRight} />
<FontAwesomeIcon
icon={faAngleDoubleRight}
className="fa-lg"
/>
</Button>
{/* spacer */}
<div className="flex-grow-1" />
<div className="align-self-center">
<img
src={logo}
Expand All @@ -158,16 +158,15 @@ const ActionPanelApp: React.FunctionComponent = () => {
className="px-4"
/>
</div>
<div className="ActionPanelToolbar">
<Button
href="/options.html"
target="_blank"
size="sm"
variant="info"
>
<FontAwesomeIcon icon={faPuzzlePiece} /> Open Extension
</Button>
</div>
<Button
href="/options.html"
target="_blank"
size="sm"
variant="link"
className="action-panel-button d-inline-flex align-items-center"
>
<span>Options <FontAwesomeIcon icon={faCog} /></span>
</Button>
</div>

<DeploymentBanner className="flex-none" />
Expand Down
6 changes: 3 additions & 3 deletions src/background/devtools/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ export const toggleSelector = liftBackground(
"TOGGLE_SELECTOR",
(target: Target) => async ({
selector,
on = true,
on,
}: {
selector: string;
on: boolean;
selector?: string;
twschiller marked this conversation as resolved.
Show resolved Hide resolved
on?: boolean;
}) => nativeEditorProtocol.toggleOverlay(target, { selector, on })
);

Expand Down
144 changes: 57 additions & 87 deletions src/devTools/editor/fields/SelectorSelectorField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,14 @@
*/

import React, {
ComponentType,
useCallback,
useContext,
useMemo,
useState,
} from "react";
import { useField } from "formik";
import { components, OptionsType } from "react-select";
import { compact, isEmpty, sortBy, uniqBy } from "lodash";
import Creatable from "react-select/creatable";
import { Badge, Button } from "react-bootstrap";
import { Button } from "react-bootstrap";
import { faMousePointer } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as nativeOperations from "@/background/devtools";
Expand All @@ -36,69 +33,26 @@ import { SelectMode } from "@/nativeEditor/selector";
import { ElementInfo } from "@/nativeEditor/frameworks";
import { Framework } from "@/messaging/constants";
import { reportError } from "@/telemetry/logging";
import { OptionProps } from "react-select/src/components/Option";
import { useToasts } from "react-toast-notifications";
import CreatableAutosuggest, { SuggestionTypeBase } from "@/devTools/editor/fields/creatableAutosuggest/CreatableAutosuggest";
import SelectorListItem from "@/devTools/editor/fields/selectorListItem/SelectorListItem";

type OptionValue = { value: string; elementInfo?: ElementInfo };
type SelectorOptions = OptionsType<OptionValue>;

const CustomOption: ComponentType<OptionProps<OptionValue, false>> = ({
children,
...props
}) => {
const { port } = useContext(DevToolsContext);

const toggle = useCallback(
async (on: boolean) => {
await nativeOperations.toggleSelector(port, {
selector: props.data.value,
on,
});
},
[port, props.data.value]
);

return (
<components.Option {...props}>
<div onMouseEnter={() => toggle(true)} onMouseLeave={() => toggle(false)}>
{props.data.elementInfo?.tagName && (
<Badge variant="dark" className="mr-1 pb-1">
{props.data.elementInfo.tagName}
</Badge>
)}
{props.data.elementInfo?.hasData && (
<Badge variant="info" className="mx-1 pb-1">
Data
</Badge>
)}
{children}
</div>
</components.Option>
);
};
interface ElementSuggestion extends SuggestionTypeBase {
value: string
elementInfo?: ElementInfo
}

function unrollValues(elementInfo: ElementInfo): OptionValue[] {
function getSuggestionsForElement(elementInfo: ElementInfo | undefined): ElementSuggestion[] {
if (!elementInfo) {
return [];
}

return [
...(elementInfo.selectors ?? []).map((value) => ({ value, elementInfo })),
...compact([elementInfo.parent]).flatMap(unrollValues),
].filter((x) => x.value && x.value.trim() !== "");
}

function makeOptions(
elementInfo: ElementInfo | null,
extra: string[] = []
): SelectorOptions {
return uniqBy(
[...unrollValues(elementInfo), ...extra.map((value) => ({ value }))],
(x) => x.value
).map((option) => ({
...option,
label: option.value,
}));
compact([
...elementInfo.selectors.map((value) => ({ value, elementInfo })),
...getSuggestionsForElement(elementInfo.parent)
]).filter((suggestion) => suggestion.value && suggestion.value.trim() !== "")
, (suggestion) => suggestion.value);
}

interface CommonProps {
Expand Down Expand Up @@ -131,14 +85,45 @@ export const SelectorSelectorControl: React.FunctionComponent<
}) => {
const { port } = useContext(DevToolsContext);
const { addToast } = useToasts();
const [element, setElement] = useState<ElementInfo>(initialElement);
const [created, setCreated] = useState([]);
const [element, setElement] = useState(initialElement);
const [isSelecting, setSelecting] = useState(false);

const options: SelectorOptions = useMemo(() => {
const raw = makeOptions(element, compact([...created, value]));
const suggestions: ElementSuggestion[] = useMemo(() => {
const raw = getSuggestionsForElement(element);
return sort ? sortBy(raw, (x) => x.value.length) : raw;
}, [created, element, value, sort]);
}, [element, sort]);

const renderSuggestion = useCallback((suggestion: ElementSuggestion) => (
twschiller marked this conversation as resolved.
Show resolved Hide resolved
<SelectorListItem
value={suggestion.value}
hasData={suggestion.elementInfo.hasData}
tag={suggestion.elementInfo.tagName}
/>
), []);

const enableSelector = useCallback((selector: string) => {
try {
void nativeOperations.toggleSelector(port, { selector });
} catch (error: unknown) {
console.debug("Error toggling selector", { selector, error });
}
},[port]);

const disableSelector = useCallback(() => {
void nativeOperations.toggleSelector(port, { on: false });
},[port]);

const onHighlighted = useCallback((suggestion: ElementSuggestion | null) => {
if (suggestion) {
enableSelector(suggestion.value);
} else {
disableSelector();
}
}, [enableSelector, disableSelector]);

const onTextChanged = useCallback((value: string) => {
twschiller marked this conversation as resolved.
Show resolved Hide resolved
onSelect(value);
}, [onSelect]);

const select = useCallback(async () => {
setSelecting(true);
Expand Down Expand Up @@ -204,31 +189,16 @@ export const SelectorSelectorControl: React.FunctionComponent<
</Button>
</div>
<div className="flex-grow-1">
<Creatable
<CreatableAutosuggest
isClearable={isClearable}
createOptionPosition="first"
isDisabled={isSelecting || disabled}
options={options}
components={{ Option: CustomOption }}
onCreateOption={(inputValue) => {
setCreated([...created, inputValue]);
onSelect(inputValue);
}}
value={options.find((x) => x.value === value)}
onMenuClose={() => {
void nativeOperations.toggleSelector(port, {
selector: null,
on: false,
});
}}
onChange={async (option) => {
console.debug("selected", { option });
onSelect(option ? option.value : null);
void nativeOperations.toggleSelector(port, {
selector: null,
on: false,
});
}}
suggestions={suggestions}
inputValue={value}
inputPlaceholder="Choose a selector..."
renderSuggestion={renderSuggestion}
onSuggestionHighlighted={onHighlighted}
onSuggestionsClosed={disableSelector}
onTextChanged={onTextChanged}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*!
* Copyright (C) 2021 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

$clearButtonPadding: 2px;

// Changes the size of the browser-native 'x' button at the end of the input
.input {
&::-webkit-search-cancel-button {
padding: $clearButtonPadding;
}
&::-ms-clear {
padding: $clearButtonPadding;
}
}

// Disables/hides the browser-native 'x' button at the end of the input
.notClearable {
// Internet Explorer
&::-ms-clear { display: none; width : 0; height: 0; }
&::-ms-reveal { display: none; width : 0; height: 0; }
// Chrome
&::-webkit-search-decoration,
&::-webkit-search-cancel-button,
&::-webkit-search-results-button,
&::-webkit-search-results-decoration { display: none; }
}
Loading