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 all 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 @@ -82,6 +83,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 @@ -210,6 +212,7 @@
"ts-loader": "^9.2.4",
"type-fest": "^2.0.0",
"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
25 changes: 25 additions & 0 deletions src/Globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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" {
const classes: Record<string, string>;
export default classes;
}
declare module "*.module.scss" {
const classes: Record<string, string>;
export default classes;
}
36 changes: 14 additions & 22 deletions src/background/devtools/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,30 +141,22 @@ export const clearDynamicElements = liftBackground(
nativeEditorProtocol.clear(target, { uuid })
);

export const toggleOverlay = liftBackground(
"TOGGLE_ELEMENT",
(target: Target) => async ({
uuid,
on = true,
}: {
uuid: string;
on: boolean;
}) =>
nativeEditorProtocol.toggleOverlay(target, {
selector: `[data-uuid="${uuid}"]`,
on,
})
export const enableDataOverlay = liftBackground(
"ENABLE_ELEMENT",
(target: Target) => async (uuid: string) =>
nativeEditorProtocol.enableOverlay(target, `[data-uuid="${uuid}"]`)
);

export const toggleSelector = liftBackground(
"TOGGLE_SELECTOR",
(target: Target) => async ({
selector,
on = true,
}: {
selector: string;
on: boolean;
}) => nativeEditorProtocol.toggleOverlay(target, { selector, on })
export const enableSelectorOverlay = liftBackground(
"ENABLE_SELECTOR",
(target: Target) => async (selector: string) =>
nativeEditorProtocol.enableOverlay(target, selector)
);

export const disableOverlay = liftBackground(
"DISABLE_ELEMENT",
(target: Target) => async () =>
nativeEditorProtocol.disableOverlay(target)
);

export const getInstalledExtensionPointIds = liftBackground(
Expand Down
152 changes: 61 additions & 91 deletions src/devTools/editor/fields/SelectorSelectorField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,92 +16,50 @@
*/

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";
import { selectElement } from "@/background/devtools";
import { selectElement, enableSelectorOverlay, disableOverlay } from "@/background/devtools";
import { DevToolsContext } from "@/devTools/context";
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={async () => toggle(true)}
onMouseLeave={async () => 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() !== "");
return uniqBy(
compact([
...(elementInfo.selectors ?? []).map((value) => ({ value, elementInfo })),
...getSuggestionsForElement(elementInfo.parent)
]).filter((suggestion) => suggestion.value && suggestion.value.trim() !== "")
, (suggestion) => suggestion.value);
}

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,
}));
function renderSuggestion(suggestion: ElementSuggestion): React.ReactNode {
return <SelectorListItem
value={suggestion.value}
hasData={suggestion.elementInfo.hasData}
tag={suggestion.elementInfo.tagName}
/>
}

interface CommonProps {
Expand Down Expand Up @@ -134,14 +92,41 @@ 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 enableSelector = useCallback((selector: string) => {
try {
void enableSelectorOverlay(port, selector);
} catch {
// The enableSelector function throws errors on invalid selector
// values, so we're eating everything here since this fires any
// time the user types in the input.
}
},[port]);

const disableSelector = useCallback(() => {
void disableOverlay(port);
},[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
disableSelector();
enableSelector(value);
onSelect(value);
}, [disableSelector, enableSelector, onSelect]);

const select = useCallback(async () => {
setSelecting(true);
Expand Down Expand Up @@ -207,31 +192,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,45 @@
/*!
* 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; }
}

.suggestionList {
max-height: 60vh;
overflow-y: scroll;
}
Loading