Plugin folders re-structuring

Summary:
Here I'm changing plugin repository structure to allow re-using of shared packages between both public and fb-internal plugins, and to ensure that public plugins has their own yarn.lock as this will be required to implement reproducible jobs checking plugin compatibility with released flipper versions.

Please note that there are a lot of moved files in this diff, make sure to click "Expand all" to see all that actually changed (there are not much of them actually).

New proposed structure for plugin packages:
```
- root
- node_modules - modules included into Flipper: flipper, flipper-plugin, react, antd, emotion
-- plugins
 --- node_modules - modules used by both public and fb-internal plugins (shared libs will be linked here, see D27034936)
 --- public
---- node_modules - modules used by public plugins
---- pluginA
----- node_modules - modules used by plugin A exclusively
---- pluginB
----- node_modules - modules used by plugin B exclusively
 --- fb
---- node_modules - modules used by fb-internal plugins
---- pluginC
----- node_modules - modules used by plugin C exclusively
---- pluginD
----- node_modules - modules used by plugin D exclusively
```
I've moved all public plugins under dir "plugins/public" and excluded them from root yarn workspaces. Instead, they will have their own yarn workspaces config and yarn.lock and they will use flipper modules as peer dependencies.

Reviewed By: mweststrate

Differential Revision: D27034108

fbshipit-source-id: c2310e3c5bfe7526033f51b46c0ae40199fd7586
This commit is contained in:
Anton Nikolaev
2021-04-09 05:15:14 -07:00
committed by Facebook GitHub Bot
parent 32bf4c32c2
commit b3274a8450
137 changed files with 2133 additions and 371 deletions

View File

@@ -0,0 +1,110 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {filterMatchPatterns} from '../util/autoCompleteProvider';
import {URI} from '../types';
// choose all k length combinations from array
const stringCombination = (patterns: Array<string>, k: number) => {
const n = patterns.length;
const returnArr: Array<string> = new Array(0);
const args = new Array(k).fill(0).map((_, idx) => idx);
(function build(args) {
const pattern = args.map((i) => patterns[i]).join('');
returnArr.push(pattern);
if (args[args.length - 1] < n - 1) {
for (let i = args.length - 1; i >= 0; i--) {
const newArgs = args.map((value, idx) =>
idx >= i ? value + 1 : value,
);
build(newArgs);
}
}
})(args);
return returnArr;
};
// Create a map of 364 pairs
const constructMatchPatterns: () => Map<string, URI> = () => {
const matchPatterns = new Map<string, URI>();
const NUM_PATERNS_PER_ENTRY = 3;
const patterns = [
'abcdefghijklmnopqrstuvwxy',
'ababababababababababababa',
'cdcdcdcdcdcdcdcdcdcdcdcdc',
'efefefefefefefefefefefefe',
'ghghghghghghghghghghghghg',
'ijijijijijijijijijijijiji',
'klklklklklklklklklklklklk',
'mnmnmnmnmnmnmnmnmnmnmnmnm',
'opopopopopopopopopopopopo',
'qrqrqrqrqrqrqrqrqrqrqrqrq',
'ststststststststststststs',
'uvuvuvuvuvuvuvuvuvuvuvuvu',
'wxwxwxwxwxwxwxwxwxwxwxwxw',
'yzyzyzyzyzyzyzyzyzyzyzyzy',
];
stringCombination(patterns, NUM_PATERNS_PER_ENTRY).forEach((pattern) =>
matchPatterns.set(pattern, pattern),
);
return matchPatterns;
};
test('construct match patterns', () => {
const matchPatterns = constructMatchPatterns();
expect(matchPatterns.size).toBe(364);
});
test('search for abcdefghijklmnopqrstuvwxy in matchPatterns', () => {
const matchPatterns = constructMatchPatterns();
const filteredMatchPatterns = filterMatchPatterns(
matchPatterns,
'abcdefghijklmnopqrstuvwxy',
Infinity,
);
// Fixing abcdefghijklmnopqrstuvwxy, we have 13C2 = 78 patterns that will match
expect(filteredMatchPatterns.size).toBe(78);
});
test('search for ????? in matchPatterns', () => {
const matchPatterns = constructMatchPatterns();
const filteredMatchPatterns = filterMatchPatterns(
matchPatterns,
'?????',
Infinity,
);
// ????? Does not exist in our seach so should return 0
expect(filteredMatchPatterns.size).toBe(0);
});
test('search for abcdefghijklmnopqrstuvwxyababababababababababababacdcdcdcdcdcdcdcdcdcdcdcdc in matchPatterns', () => {
const matchPatterns = constructMatchPatterns();
const filteredMatchPatterns = filterMatchPatterns(
matchPatterns,
'abcdefghijklmnopqrstuvwxyababababababababababababacdcdcdcdcdcdcdcdcdcdcdcdc',
Infinity,
);
// Should only appear once in our patterns
expect(filteredMatchPatterns.size).toBe(1);
});
test('find first five occurences of abcdefghijklmnopqrstuvwxy', () => {
const matchPatterns = constructMatchPatterns();
const filteredMatchPatterns = filterMatchPatterns(
matchPatterns,
'abcdefghijklmnopqrstuvwxy',
5,
);
expect(filteredMatchPatterns.size).toBe(5);
});

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
getRequiredParameters,
parameterIsNumberType,
replaceRequiredParametersWithValues,
filterOptionalParameters,
} from '../util/uri';
test('parse required parameters from uri', () => {
const testURI =
'fb://test_uri/?parameter1={parameter1}&parameter2={parameter2}';
const expectedResult = ['{parameter1}', '{parameter2}'];
expect(getRequiredParameters(testURI)).toEqual(expectedResult);
});
test('parse required numeric parameters from uri', () => {
const testURI =
'fb://test_uri/?parameter1={#parameter1}&parameter2={#parameter2}';
const expectedResult = ['{#parameter1}', '{#parameter2}'];
expect(getRequiredParameters(testURI)).toEqual(expectedResult);
});
test('replace required parameters with values', () => {
const testURI =
'fb://test_uri/?parameter1={parameter1}&parameter2={parameter2}';
const expectedResult = 'fb://test_uri/?parameter1=okay&parameter2=sure';
expect(
replaceRequiredParametersWithValues(testURI, ['okay', 'sure']),
).toEqual(expectedResult);
});
test('skip non-required parameters in replacement', () => {
const testURI =
'fb://test_uri/?parameter1={parameter1}&parameter2={?parameter2}&parameter3={parameter3}';
const expectedResult =
'fb://test_uri/?parameter1=okay&parameter2={?parameter2}&parameter3=sure';
expect(
replaceRequiredParametersWithValues(testURI, ['okay', 'sure']),
).toEqual(expectedResult);
});
test('detect if required parameter is numeric type', () => {
expect(parameterIsNumberType('{#numerictype}')).toBe(true);
});
test('detect if required parameter is not numeric type', () => {
expect(parameterIsNumberType('{numerictype}')).toBe(false);
});
test('filter optional parameters from uri', () => {
const testURI =
'fb://test_uri/{?param_here}/?parameter1={parameter1}&parameter2={?parameter2}&numericParameter={#numericParameter}&parameter3={?parameter3}';
const expextedResult =
'fb://test_uri/?parameter1={parameter1}&numericParameter={#numericParameter}';
expect(filterOptionalParameters(testURI)).toBe(expextedResult);
});

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Glyph, styled} from 'flipper';
import {useItemNavigation} from '../hooks/autoCompleteSheet';
import {filterProvidersToLineItems} from '../util/autoCompleteProvider';
import {AutoCompleteProvider, AutoCompleteLineItem, URI} from '../types';
import React from 'react';
type Props = {
providers: Array<AutoCompleteProvider>;
onHighlighted: (uri: URI) => void;
onNavigate: (uri: URI) => void;
query: string;
};
const MAX_ITEMS = 5;
const AutoCompleteSheetContainer = styled.div({
width: '100%',
position: 'absolute',
top: 'calc(100% - 3px)',
backgroundColor: 'white',
zIndex: 1,
borderBottomRightRadius: 10,
borderBottomLeftRadius: 10,
boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
});
const SheetItem = styled.div({
padding: 5,
textOverflow: 'ellipsis',
overflowX: 'hidden',
whiteSpace: 'nowrap',
'&.selected': {
backgroundColor: 'rgba(155, 155, 155, 0.2)',
},
'&:hover': {
backgroundColor: 'rgba(155, 155, 155, 0.2)',
},
});
const SheetItemIcon = styled.span({
padding: 8,
});
export default (props: Props) => {
const {providers, onHighlighted, onNavigate, query} = props;
const lineItems = filterProvidersToLineItems(providers, query, MAX_ITEMS);
lineItems.unshift({uri: query, matchPattern: query, icon: 'send'});
const selectedItem = useItemNavigation(lineItems, onHighlighted);
return (
<AutoCompleteSheetContainer>
{lineItems.map((lineItem: AutoCompleteLineItem, idx: number) => (
<SheetItem
className={idx === selectedItem ? 'selected' : ''}
key={idx}
onMouseDown={() => onNavigate(lineItem.uri)}>
<SheetItemIcon>
<Glyph name={lineItem.icon} size={16} variant="outline" />
</SheetItemIcon>
{lineItem.matchPattern}
</SheetItem>
))}
</AutoCompleteSheetContainer>
);
};

View File

@@ -0,0 +1,126 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
DetailSidebar,
FlexCenter,
styled,
colors,
FlexRow,
FlexColumn,
Text,
Panel,
} from 'flipper';
import {Bookmark, URI} from '../types';
import {IconButton} from './';
import React from 'react';
type Props = {
bookmarks: Map<string, Bookmark>;
onNavigate: (uri: URI) => void;
onRemove: (uri: URI) => void;
};
const NoData = styled(FlexCenter)({
fontSize: 18,
color: colors.macOSTitleBarIcon,
});
const BookmarksList = styled.div({
overflowY: 'scroll',
overflowX: 'hidden',
height: '100%',
backgroundColor: colors.white,
});
const BookmarkContainer = styled(FlexRow)({
width: '100%',
padding: 10,
height: 55,
alignItems: 'center',
cursor: 'pointer',
borderBottom: `1px ${colors.greyTint} solid`,
':last-child': {
borderBottom: '0',
},
':active': {
backgroundColor: colors.highlight,
color: colors.white,
},
':active *': {
color: colors.white,
},
});
const BookmarkTitle = styled(Text)({
fontSize: '1.1em',
overflowX: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
fontWeight: 500,
});
const BookmarkSubtitle = styled(Text)({
overflowX: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
color: colors.greyTint3,
marginTop: 4,
});
const TextContainer = styled(FlexColumn)({
justifyContent: 'center',
});
const alphabetizeBookmarkCompare = (b1: Bookmark, b2: Bookmark) => {
return b1.uri < b2.uri ? -1 : b1.uri > b2.uri ? 1 : 0;
};
export default (props: Props) => {
const {bookmarks, onNavigate, onRemove} = props;
return (
<DetailSidebar>
<Panel heading="Bookmarks" floating={false} padded={false}>
{bookmarks.size === 0 ? (
<NoData grow>No Bookmarks</NoData>
) : (
<BookmarksList>
{[...bookmarks.values()]
.sort(alphabetizeBookmarkCompare)
.map((bookmark, idx) => (
<BookmarkContainer
key={idx}
role="button"
tabIndex={0}
onClick={() => {
onNavigate(bookmark.uri);
}}>
<TextContainer grow>
<BookmarkTitle>
{bookmark.commonName || bookmark.uri}
</BookmarkTitle>
{!bookmark.commonName && (
<BookmarkSubtitle>{bookmark.uri}</BookmarkSubtitle>
)}
</TextContainer>
<IconButton
color={colors.macOSTitleBarButtonBackgroundActive}
outline={false}
icon="cross-circle"
size={16}
onClick={() => onRemove(bookmark.uri)}
/>
</BookmarkContainer>
))}
</BookmarksList>
)}
</Panel>
</DetailSidebar>
);
};

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {styled, IconSize, colors} from 'flipper';
import {IconButton} from './';
import React from 'react';
type Props = {
onClick?: () => void;
highlighted: boolean;
size: IconSize;
};
const FavoriteButtonContainer = styled.div({
position: 'relative',
'>:first-child': {
position: 'absolute',
},
'>:last-child': {
position: 'relative',
},
});
export default (props: Props) => {
const {highlighted, onClick, ...iconButtonProps} = props;
return (
<FavoriteButtonContainer>
{highlighted ? (
<IconButton
outline={false}
color={colors.lemon}
icon="star"
{...iconButtonProps}
/>
) : null}
<IconButton
outline={true}
icon="star"
onClick={onClick}
{...iconButtonProps}
/>
</FavoriteButtonContainer>
);
};

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Glyph, styled, keyframes, IconSize} from 'flipper';
import React from 'react';
const shrinkAnimation = keyframes({
'0%': {
transform: 'scale(1);',
},
'100%': {
transform: 'scale(.9)',
},
});
type Props = {
icon: string;
outline?: boolean;
onClick?: () => void;
color?: string;
size: IconSize;
};
const RippleEffect = styled.div({
padding: 5,
borderRadius: 100,
backgroundPosition: 'center',
transition: 'background 0.5s',
':hover': {
background:
'rgba(155, 155, 155, 0.2) radial-gradient(circle, transparent 1%, rgba(155, 155, 155, 0.2) 1%) center/15000%',
},
':active': {
backgroundColor: 'rgba(201, 200, 200, 0.5)',
backgroundSize: '100%',
transition: 'background 0s',
},
});
const IconButton = styled.div({
':active': {
animation: `${shrinkAnimation} .25s ease forwards`,
},
});
export default function (props: Props) {
return (
<RippleEffect>
<IconButton className="icon-button" onClick={props.onClick}>
<Glyph
name={props.icon}
size={props.size}
color={props.color}
variant={props.outline ? 'outline' : 'filled'}
/>
</IconButton>
</RippleEffect>
);
}

View File

@@ -0,0 +1,240 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
styled,
colors,
ManagedTable,
TableBodyRow,
FlexCenter,
LoadingIndicator,
Button,
Glyph,
} from 'flipper';
import {parseURIParameters, stripQueryParameters} from '../util/uri';
import React from 'react';
const BOX_HEIGHT = 240;
type Props = {
isBookmarked: boolean;
uri: string | null;
className: string | null;
onNavigate: (query: string) => void;
onFavorite: (query: string) => void;
screenshot: string | null;
date: Date | null;
};
const ScreenshotContainer = styled.div({
width: 200,
minWidth: 200,
overflow: 'hidden',
borderLeft: `1px ${colors.blueGreyTint90} solid`,
position: 'relative',
height: '100%',
borderRadius: 10,
img: {
width: '100%',
},
});
const NoData = styled.div({
color: colors.light30,
fontSize: 14,
position: 'relative',
});
const NavigationDataContainer = styled.div({
alignItems: 'flex-start',
flexGrow: 1,
position: 'relative',
});
const Footer = styled.div({
width: '100%',
padding: '10px',
borderTop: `1px ${colors.blueGreyTint90} solid`,
display: 'flex',
alignItems: 'center',
});
const Seperator = styled.div({
flexGrow: 1,
});
const TimeContainer = styled.div({
color: colors.light30,
fontSize: 14,
});
const NavigationInfoBoxContainer = styled.div({
display: 'flex',
height: BOX_HEIGHT,
borderRadius: 10,
flexGrow: 1,
position: 'relative',
marginBottom: 10,
backgroundColor: colors.white,
boxShadow: '1px 1px 5px rgba(0,0,0,0.1)',
});
const Header = styled.div({
fontSize: 18,
fontWeight: 500,
userSelect: 'text',
cursor: 'text',
padding: 10,
borderBottom: `1px ${colors.blueGreyTint90} solid`,
display: 'flex',
});
const ClassNameContainer = styled.div({
color: colors.light30,
});
const ParametersContainer = styled.div({
height: 150,
'&>*': {
height: 150,
marginBottom: 20,
},
});
const NoParamters = styled(FlexCenter)({
fontSize: 18,
color: colors.light10,
});
const TimelineCircle = styled.div({
width: 18,
height: 18,
top: 11,
left: -33,
backgroundColor: colors.light02,
border: `4px solid ${colors.highlight}`,
borderRadius: '50%',
position: 'absolute',
});
const TimelineMiniCircle = styled.div({
width: 12,
height: 12,
top: 1,
left: -30,
borderRadius: '50%',
backgroundColor: colors.highlight,
position: 'absolute',
});
const buildParameterTable = (parameters: Map<string, string>) => {
const tableRows: Array<TableBodyRow> = [];
let idx = 0;
parameters.forEach((parameter_value, parameter) => {
tableRows.push({
key: idx.toString(),
columns: {
parameter: {
value: parameter,
},
value: {
value: parameter_value,
},
},
});
idx++;
});
return (
<ManagedTable
columns={{parameter: {value: 'Parameter'}, value: {value: 'Value'}}}
rows={tableRows}
zebra={false}
/>
);
};
export default (props: Props) => {
const {
uri,
isBookmarked,
className,
screenshot,
onNavigate,
onFavorite,
date,
} = props;
if (uri == null && className == null) {
return (
<>
<NoData>
<TimelineMiniCircle />
Unknown Navigation Event
</NoData>
</>
);
} else {
const parameters = uri != null ? parseURIParameters(uri) : null;
return (
<NavigationInfoBoxContainer>
<TimelineCircle />
<NavigationDataContainer>
<Header>
{uri != null ? stripQueryParameters(uri) : ''}
<Seperator />
{className != null ? (
<>
<Glyph
color={colors.light30}
size={16}
name="paper-fold-text"
/>
&nbsp;
<ClassNameContainer>
{className != null ? className : ''}
</ClassNameContainer>
</>
) : null}
</Header>
<ParametersContainer>
{parameters != null && parameters.size > 0 ? (
buildParameterTable(parameters)
) : (
<NoParamters grow>No Parameters for this Event</NoParamters>
)}
</ParametersContainer>
<Footer>
{uri != null ? (
<>
<Button onClick={() => onNavigate(uri)}>Open</Button>
<Button onClick={() => onFavorite(uri)}>
{isBookmarked ? 'Edit Bookmark' : 'Bookmark'}
</Button>
</>
) : null}
<Seperator />
<TimeContainer>
{date != null ? date.toTimeString() : ''}
</TimeContainer>
</Footer>
</NavigationDataContainer>
{uri != null || className != null ? (
<ScreenshotContainer>
{screenshot != null ? (
<img src={screenshot} />
) : (
<FlexCenter grow>
<LoadingIndicator size={32} />
</FlexCenter>
)}
</ScreenshotContainer>
) : null}
</NavigationInfoBoxContainer>
);
}
};

View File

@@ -0,0 +1,99 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Modal, Button, Alert, Input, Typography} from 'antd';
import {Layout} from 'flipper-plugin';
import {
replaceRequiredParametersWithValues,
parameterIsNumberType,
parameterIsBooleanType,
validateParameter,
liveEdit,
} from '../util/uri';
import {useRequiredParameterFormValidator} from '../hooks/requiredParameters';
import React from 'react';
import {URI} from '../types';
type Props = {
uri: string;
requiredParameters: Array<string>;
onHide: () => void;
onSubmit: (uri: URI) => void;
};
export default (props: Props) => {
const {onHide, onSubmit, uri, requiredParameters} = props;
const {isValid, values, setValuesArray} = useRequiredParameterFormValidator(
requiredParameters,
);
return (
<Modal
visible
onCancel={onHide}
title="Provide bookmark details"
footer={
<>
<Button
onClick={() => {
onHide();
setValuesArray([]);
}}>
Cancel
</Button>
<Button
type={'primary'}
onClick={() => {
onSubmit(replaceRequiredParametersWithValues(uri, values));
onHide();
}}
disabled={!isValid}>
Submit
</Button>
</>
}>
<Layout.Container gap>
<Alert
type="info"
message="This uri has required parameters denoted by '{parameter}'}."
/>
{requiredParameters.map((paramater, idx) => (
<div key={idx}>
<Input
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setValuesArray([
...values.slice(0, idx),
event.target.value,
...values.slice(idx + 1),
])
}
name={paramater}
placeholder={paramater}
/>
{values[idx] &&
parameterIsNumberType(paramater) &&
!validateParameter(values[idx], paramater) ? (
<Alert type="error" message="Parameter must be a number" />
) : null}
{values[idx] &&
parameterIsBooleanType(paramater) &&
!validateParameter(values[idx], paramater) ? (
<Alert
type="error"
message="Parameter must be either 'true' or 'false'"
/>
) : null}
</div>
))}
<Typography.Text code>{liveEdit(uri, values)}</Typography.Text>
</Layout.Container>
</Modal>
);
};

View File

@@ -0,0 +1,117 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Button, FlexColumn, Input, Sheet, styled} from 'flipper';
import React, {useState} from 'react';
import {Bookmark, URI} from '../types';
type Props = {
uri: string | null;
edit: boolean;
shouldShow: boolean;
onHide?: () => void;
onRemove: (uri: URI) => void;
onSubmit: (bookmark: Bookmark) => void;
};
const Container = styled(FlexColumn)({
padding: 10,
width: 400,
});
const Title = styled.div({
fontWeight: 500,
marginTop: 8,
marginLeft: 2,
marginBottom: 8,
});
const URIContainer = styled.div({
marginLeft: 2,
marginBottom: 8,
overflowWrap: 'break-word',
});
const ButtonContainer = styled.div({
marginLeft: 'auto',
});
const NameInput = styled(Input)({
margin: 0,
marginBottom: 10,
height: 30,
});
export default (props: Props) => {
const {edit, shouldShow, onHide, onRemove, onSubmit, uri} = props;
const [commonName, setCommonName] = useState('');
if (uri == null || !shouldShow) {
return null;
} else {
return (
<Sheet onHideSheet={onHide}>
{(onHide: () => void) => {
return (
<Container>
<Title>
{edit ? 'Edit bookmark...' : 'Save to bookmarks...'}
</Title>
<NameInput
placeholder="Name..."
value={commonName}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setCommonName(event.target.value)
}
/>
<URIContainer>{uri}</URIContainer>
<ButtonContainer>
<Button
onClick={() => {
onHide();
setCommonName('');
}}
compact
padded>
Cancel
</Button>
{edit ? (
<Button
type="danger"
onClick={() => {
onHide();
onRemove(uri);
setCommonName('');
}}
compact
padded>
Remove
</Button>
) : null}
<Button
type="primary"
onClick={() => {
onHide();
onSubmit({uri, commonName});
// The component state is remembered even after unmounting.
// Thus it is necessary to reset the commonName here.
setCommonName('');
}}
compact
padded>
Save
</Button>
</ButtonContainer>
</Container>
);
}}
</Sheet>
);
}
};

View File

@@ -0,0 +1,166 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {styled, SearchBox, SearchInput, Toolbar} from 'flipper';
import {AutoCompleteSheet, IconButton, FavoriteButton} from './';
import {AutoCompleteProvider, Bookmark, URI} from '../types';
import React, {Component} from 'react';
type Props = {
onFavorite: (query: URI) => void;
onNavigate: (query: URI) => void;
bookmarks: Map<URI, Bookmark>;
providers: Array<AutoCompleteProvider>;
uriFromAbove: URI;
};
type State = {
query: URI;
inputFocused: boolean;
autoCompleteSheetOpen: boolean;
searchInputValue: URI;
prevURIFromAbove: URI;
};
const IconContainer = styled.div({
display: 'inline-flex',
height: '16px',
alignItems: 'center',
'': {
marginLeft: 10,
'.icon-button': {
height: 16,
},
'img,div': {
verticalAlign: 'top',
alignItems: 'none',
},
},
});
const ToolbarContainer = styled.div({
'.drop-shadow': {
boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
},
});
const SearchInputContainer = styled.div({
width: '100%',
marginLeft: 5,
marginRight: 9,
position: 'relative',
});
class SearchBar extends Component<Props, State> {
state = {
inputFocused: false,
autoCompleteSheetOpen: false,
query: '',
searchInputValue: '',
prevURIFromAbove: '',
};
favorite = (searchInputValue: string) => {
this.props.onFavorite(searchInputValue);
};
navigateTo = (searchInputValue: string) => {
this.setState({query: searchInputValue, searchInputValue});
this.props.onNavigate(searchInputValue);
};
queryInputChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
this.setState({query: value, searchInputValue: value});
};
static getDerivedStateFromProps = (newProps: Props, state: State) => {
const {uriFromAbove: newURIFromAbove} = newProps;
const {prevURIFromAbove} = state;
if (newURIFromAbove !== prevURIFromAbove) {
return {
searchInputValue: newURIFromAbove,
query: newURIFromAbove,
prevURIFromAbove: newURIFromAbove,
};
}
return null;
};
render() {
const {bookmarks, providers} = this.props;
const {
autoCompleteSheetOpen,
inputFocused,
searchInputValue,
query,
} = this.state;
return (
<ToolbarContainer>
<Toolbar>
<SearchBox className={inputFocused ? 'drop-shadow' : ''}>
<SearchInputContainer>
<SearchInput
value={searchInputValue}
onBlur={() =>
this.setState({
autoCompleteSheetOpen: false,
inputFocused: false,
})
}
onFocus={(event: React.FocusEvent<HTMLInputElement>) => {
event.target.select();
this.setState({
autoCompleteSheetOpen: true,
inputFocused: true,
});
}}
onChange={this.queryInputChanged}
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
this.navigateTo(this.state.searchInputValue);
(e.target as HTMLInputElement).blur();
}
}}
placeholder="Navigate To..."
/>
{autoCompleteSheetOpen && query.length > 0 ? (
<AutoCompleteSheet
providers={providers}
onNavigate={this.navigateTo}
onHighlighted={(newInputValue: URI) =>
this.setState({searchInputValue: newInputValue})
}
query={query}
/>
) : null}
</SearchInputContainer>
</SearchBox>
{searchInputValue.length > 0 ? (
<IconContainer>
<IconButton
icon="send"
size={16}
outline={true}
onClick={() => this.navigateTo(searchInputValue)}
/>
<FavoriteButton
size={16}
highlighted={bookmarks.has(searchInputValue)}
onClick={() => this.favorite(searchInputValue)}
/>
</IconContainer>
) : null}
</Toolbar>
</ToolbarContainer>
);
}
}
export default SearchBar;

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {colors, FlexCenter, styled} from 'flipper';
import {NavigationInfoBox} from './';
import {Bookmark, NavigationEvent, URI} from '../types';
import React, {useRef} from 'react';
type Props = {
bookmarks: Map<string, Bookmark>;
events: Array<NavigationEvent>;
onNavigate: (uri: URI) => void;
onFavorite: (uri: URI) => void;
};
const TimelineLine = styled.div({
width: 2,
backgroundColor: colors.highlight,
position: 'absolute',
top: 38,
bottom: 0,
});
const TimelineContainer = styled.div({
position: 'relative',
paddingLeft: 25,
overflowY: 'scroll',
flexGrow: 1,
backgroundColor: colors.light02,
scrollBehavior: 'smooth',
'&>div': {
position: 'relative',
minHeight: '100%',
'&:last-child': {
paddingBottom: 25,
},
},
});
const NavigationEventContainer = styled.div({
display: 'flex',
paddingTop: 25,
paddingLeft: 25,
marginRight: 25,
});
const NoData = styled(FlexCenter)({
height: '100%',
fontSize: 18,
backgroundColor: colors.macOSTitleBarBackgroundBlur,
color: colors.macOSTitleBarIcon,
});
export default (props: Props) => {
const {bookmarks, events, onNavigate, onFavorite} = props;
const timelineRef = useRef<HTMLDivElement>(null);
return events.length === 0 ? (
<NoData>No Navigation Events to Show</NoData>
) : (
<TimelineContainer ref={timelineRef}>
<div>
<TimelineLine />
{events.map((event: NavigationEvent, idx: number) => {
return (
<NavigationEventContainer key={idx}>
<NavigationInfoBox
isBookmarked={
event.uri != null ? bookmarks.has(event.uri) : false
}
className={event.className}
uri={event.uri}
onNavigate={(uri) => {
if (timelineRef.current != null) {
timelineRef.current.scrollTo(0, 0);
}
onNavigate(uri);
}}
onFavorite={onFavorite}
screenshot={event.screenshot}
date={event.date}
/>
</NavigationEventContainer>
);
})}
</div>
</TimelineContainer>
);
};

View File

@@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
export {default as AutoCompleteSheet} from './AutoCompleteSheet';
export {default as BookmarksSidebar} from './BookmarksSidebar';
export {default as FavoriteButton} from './FavoriteButton';
export {default as IconButton} from './IconButton';
export {default as NavigationInfoBox} from './NavigationInfoBox';
export {default as RequiredParametersDialog} from './RequiredParametersDialog';
export {default as SaveBookmarkDialog} from './SaveBookmarkDialog';
export {default as SearchBar} from './SearchBar';
export {default as Timeline} from './Timeline';

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {useEffect, useState} from 'react';
import {AutoCompleteLineItem} from '../types';
export const useItemNavigation = (
lineItems: Array<AutoCompleteLineItem>,
onHighlighted: (uri: string) => void,
) => {
const [selectedItem, setSelectedItem] = useState(0);
const handleKeyPress = (event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowDown': {
const newSelectedItem =
selectedItem < lineItems.length - 1
? selectedItem + 1
: lineItems.length - 1;
setSelectedItem(newSelectedItem);
onHighlighted(lineItems[newSelectedItem].uri);
break;
}
case 'ArrowUp': {
const newSelectedItem =
selectedItem > 0 ? selectedItem - 1 : selectedItem;
setSelectedItem(newSelectedItem);
onHighlighted(lineItems[newSelectedItem].uri);
break;
}
default: {
setSelectedItem(0);
break;
}
}
};
useEffect(() => {
window.addEventListener('keydown', handleKeyPress);
return () => {
window.removeEventListener('keydown', handleKeyPress);
};
});
return selectedItem;
};

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {useMemo, useState} from 'react';
import {validateParameter} from '../util/uri';
export const useRequiredParameterFormValidator = (
requiredParameters: Array<string>,
) => {
const [values, setValuesArray] = useState<Array<string>>(
requiredParameters.map(() => ''),
);
const isValid = useMemo(() => {
if (requiredParameters.length != values.length) {
setValuesArray(requiredParameters.map(() => ''));
}
if (
values.every((value, idx) =>
validateParameter(value, requiredParameters[idx]),
)
) {
return true;
} else {
return false;
}
}, [requiredParameters, values]);
return {isValid, values, setValuesArray};
};

View File

@@ -0,0 +1,255 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
import {bufferToBlob} from 'flipper';
import {
BookmarksSidebar,
SaveBookmarkDialog,
SearchBar,
Timeline,
RequiredParametersDialog,
} from './components';
import {
removeBookmarkFromDB,
readBookmarksFromDB,
writeBookmarkToDB,
} from './util/indexedDB';
import {
appMatchPatternsToAutoCompleteProvider,
bookmarksToAutoCompleteProvider,
} from './util/autoCompleteProvider';
import {getAppMatchPatterns} from './util/appMatchPatterns';
import {getRequiredParameters, filterOptionalParameters} from './util/uri';
import {
Bookmark,
NavigationEvent,
AppMatchPattern,
URI,
RawNavigationEvent,
} from './types';
import React, {useMemo} from 'react';
import {
PluginClient,
createState,
useValue,
usePlugin,
Layout,
renderReactRoot,
} from 'flipper-plugin';
export type State = {
shouldShowSaveBookmarkDialog: boolean;
shouldShowURIErrorDialog: boolean;
saveBookmarkURI: URI | null;
requiredParameters: Array<string>;
};
type Events = {
nav_event: RawNavigationEvent;
};
type Methods = {
navigate_to(params: {url: string}): Promise<void>;
};
export type NavigationPlugin = ReturnType<typeof plugin>;
export function plugin(client: PluginClient<Events, Methods>) {
const bookmarks = createState(new Map<URI, Bookmark>(), {
persist: 'bookmarks',
});
const navigationEvents = createState<NavigationEvent[]>([], {
persist: 'navigationEvents',
});
const appMatchPatterns = createState<AppMatchPattern[]>([], {
persist: 'appMatchPatterns',
});
const currentURI = createState('');
const shouldShowSaveBookmarkDialog = createState(false);
const saveBookmarkURI = createState<null | string>(null);
client.onMessage('nav_event', async (payload) => {
const navigationEvent: NavigationEvent = {
uri: payload.uri === undefined ? null : decodeURIComponent(payload.uri),
date: payload.date ? new Date(payload.date) : new Date(),
className: payload.class === undefined ? null : payload.class,
screenshot: null,
};
if (navigationEvent.uri) currentURI.set(navigationEvent.uri);
navigationEvents.update((draft) => {
draft.unshift(navigationEvent);
});
const screenshot: Buffer = await client.device.realDevice.screenshot();
const blobURL = URL.createObjectURL(bufferToBlob(screenshot));
// this process is async, make sure we update the correct one..
const navigationEventIndex = navigationEvents
.get()
.indexOf(navigationEvent);
if (navigationEventIndex !== -1) {
navigationEvents.update((draft) => {
draft[navigationEventIndex].screenshot = blobURL;
});
}
});
getAppMatchPatterns(client.appId, client.device.realDevice)
.then((patterns) => {
appMatchPatterns.set(patterns);
})
.catch((e) => {
console.error('[Navigation] Failed to find appMatchPatterns', e);
});
readBookmarksFromDB().then((bookmarksData) => {
bookmarks.set(bookmarksData);
});
function navigateTo(query: string) {
const filteredQuery = filterOptionalParameters(query);
currentURI.set(filteredQuery);
const params = getRequiredParameters(filteredQuery);
if (params.length === 0) {
if (client.appName === 'Facebook' && client.device.os === 'iOS') {
// use custom navigate_to event for Wilde
client.send('navigate_to', {
url: filterOptionalParameters(filteredQuery),
});
} else {
client.device.realDevice.navigateToLocation(
filterOptionalParameters(filteredQuery),
);
}
} else {
renderReactRoot((unmount) => (
<RequiredParametersDialog
onHide={unmount}
uri={filteredQuery}
requiredParameters={params}
onSubmit={navigateTo}
/>
));
}
}
function onFavorite(uri: string) {
shouldShowSaveBookmarkDialog.set(true);
saveBookmarkURI.set(uri);
}
function addBookmark(bookmark: Bookmark) {
const newBookmark = {
uri: bookmark.uri,
commonName: bookmark.commonName,
};
bookmarks.update((draft) => {
draft.set(newBookmark.uri, newBookmark);
});
writeBookmarkToDB(newBookmark);
}
function removeBookmark(uri: string) {
bookmarks.update((draft) => {
draft.delete(uri);
});
removeBookmarkFromDB(uri);
}
return {
navigateTo,
onFavorite,
addBookmark,
removeBookmark,
bookmarks,
saveBookmarkURI,
shouldShowSaveBookmarkDialog,
appMatchPatterns,
navigationEvents,
currentURI,
getAutoCompleteAppMatchPatterns(
query: string,
bookmarks: Map<string, Bookmark>,
appMatchPatterns: AppMatchPattern[],
limit: number,
): AppMatchPattern[] {
const q = query.toLowerCase();
const results: AppMatchPattern[] = [];
for (const item of appMatchPatterns) {
if (
!bookmarks.has(item.pattern) &&
(item.className.toLowerCase().includes(q) ||
item.pattern.toLowerCase().includes(q))
) {
results.push(item);
if (--limit < 1) break;
}
}
return results;
},
};
}
export function Component() {
const instance = usePlugin(plugin);
const bookmarks = useValue(instance.bookmarks);
const appMatchPatterns = useValue(instance.appMatchPatterns);
const saveBookmarkURI = useValue(instance.saveBookmarkURI);
const shouldShowSaveBookmarkDialog = useValue(
instance.shouldShowSaveBookmarkDialog,
);
const currentURI = useValue(instance.currentURI);
const navigationEvents = useValue(instance.navigationEvents);
const autoCompleteProviders = useMemo(
() => [
bookmarksToAutoCompleteProvider(bookmarks),
appMatchPatternsToAutoCompleteProvider(appMatchPatterns),
],
[bookmarks, appMatchPatterns],
);
return (
<Layout.Container>
<SearchBar
providers={autoCompleteProviders}
bookmarks={bookmarks}
onNavigate={instance.navigateTo}
onFavorite={instance.onFavorite}
uriFromAbove={currentURI}
/>
<Timeline
bookmarks={bookmarks}
events={navigationEvents}
onNavigate={instance.navigateTo}
onFavorite={instance.onFavorite}
/>
<BookmarksSidebar
bookmarks={bookmarks}
onRemove={instance.removeBookmark}
onNavigate={instance.navigateTo}
/>
<SaveBookmarkDialog
shouldShow={shouldShowSaveBookmarkDialog}
uri={saveBookmarkURI}
onHide={() => {
instance.shouldShowSaveBookmarkDialog.set(false);
}}
edit={saveBookmarkURI != null ? bookmarks.has(saveBookmarkURI) : false}
onSubmit={instance.addBookmark}
onRemove={instance.removeBookmark}
/>
</Layout.Container>
);
}
/* @scarf-info: do not remove, more info: https://fburl.com/scarf */
/* @scarf-generated: flipper-plugin index.js.template 0bfa32e5-fb15-4705-81f8-86260a1f3f8e */

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://fbflipper.com/schemas/plugin-package/v2.json",
"name": "flipper-plugin-navigation",
"id": "Navigation",
"version": "0.0.0",
"main": "dist/bundle.js",
"flipperBundlerEntry": "index.tsx",
"license": "MIT",
"keywords": [
"flipper-plugin"
],
"title": "Navigation",
"icon": "directions",
"bugs": {
"email": "beneloca@fb.com"
},
"peerDependencies": {
"flipper-plugin": "*",
"antd": "*"
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": ".",
"esModuleInterop": true
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
export type URI = string;
export type RawNavigationEvent = {
date: string | undefined;
uri: URI | undefined;
class: string | undefined;
screenshot: string | undefined;
};
export type NavigationEvent = {
date: Date | null;
uri: URI | null;
className: string | null;
screenshot: string | null;
};
export type Bookmark = {
uri: URI;
commonName: string | null;
};
export type AutoCompleteProvider = {
icon: string;
matchPatterns: Map<string, URI>;
};
export type AutoCompleteLineItem = {
icon: string;
matchPattern: string;
uri: URI;
};
export type AppMatchPattern = {
className: string;
pattern: string;
};

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import fs from 'fs';
import path from 'path';
import {BaseDevice, AndroidDevice, IOSDevice} from 'flipper';
import {AppMatchPattern} from '../types';
import {remote} from 'electron';
let patternsPath: string | undefined;
function getPatternsBasePath() {
return (patternsPath =
patternsPath ?? path.join(remote.app.getAppPath(), 'facebook'));
}
const extractAppNameFromSelectedApp = (selectedApp: string | null) => {
if (selectedApp == null) {
return null;
} else {
return selectedApp.split('#')[0];
}
};
export const getAppMatchPatterns = (
selectedApp: string | null,
device: BaseDevice,
) => {
return new Promise<Array<AppMatchPattern>>((resolve, reject) => {
const appName = extractAppNameFromSelectedApp(selectedApp);
if (appName === 'Facebook') {
let filename: string;
if (device instanceof AndroidDevice) {
filename = 'facebook-match-patterns-android.json';
} else if (device instanceof IOSDevice) {
filename = 'facebook-match-patterns-ios.json';
} else {
return;
}
const patternsFilePath = path.join(getPatternsBasePath(), filename);
fs.readFile(patternsFilePath, (err, data) => {
if (err) {
reject(err);
} else {
resolve(JSON.parse(data.toString()));
}
});
} else if (appName != null) {
console.log('No rule for app ' + appName);
resolve([]);
} else {
reject(new Error('selectedApp was null'));
}
});
};

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
URI,
Bookmark,
AutoCompleteProvider,
AutoCompleteLineItem,
AppMatchPattern,
} from '../types';
export function DefaultProvider(): AutoCompleteProvider {
return {
icon: 'caution',
matchPatterns: new Map<string, URI>(),
};
}
export const bookmarksToAutoCompleteProvider = (
bookmarks: Map<URI, Bookmark>,
) => {
const autoCompleteProvider = {
icon: 'bookmark',
matchPatterns: new Map<string, URI>(),
} as AutoCompleteProvider;
bookmarks.forEach((bookmark, uri) => {
const matchPattern = bookmark.commonName + ' - ' + uri;
autoCompleteProvider.matchPatterns.set(matchPattern, uri);
});
return autoCompleteProvider;
};
export const appMatchPatternsToAutoCompleteProvider = (
appMatchPatterns: Array<AppMatchPattern>,
) => {
const autoCompleteProvider = {
icon: 'mobile',
matchPatterns: new Map<string, URI>(),
};
appMatchPatterns.forEach((appMatchPattern) => {
const matchPattern =
appMatchPattern.className + ' - ' + appMatchPattern.pattern;
autoCompleteProvider.matchPatterns.set(
matchPattern,
appMatchPattern.pattern,
);
});
return autoCompleteProvider;
};
export const filterMatchPatterns = (
matchPatterns: Map<string, URI>,
query: URI,
maxItems: number,
) => {
const filteredPatterns = new Map<string, URI>();
for (const [pattern, uri] of matchPatterns) {
if (filteredPatterns.size >= maxItems) {
break;
} else if (pattern.toLowerCase().includes(query.toLowerCase())) {
filteredPatterns.set(pattern, uri);
}
}
return filteredPatterns;
};
const filterProvider = (
provider: AutoCompleteProvider,
query: string,
maxItems: number,
) => {
return {
...provider,
matchPatterns: filterMatchPatterns(provider.matchPatterns, query, maxItems),
};
};
export const filterProvidersToLineItems = (
providers: Array<AutoCompleteProvider>,
query: string,
maxItems: number,
) => {
let itemsLeft = maxItems;
const lineItems = new Array<AutoCompleteLineItem>(0);
for (const provider of providers) {
const filteredProvider = filterProvider(provider, query, itemsLeft);
filteredProvider.matchPatterns.forEach((uri, matchPattern) => {
lineItems.push({
icon: provider.icon,
matchPattern,
uri,
});
});
itemsLeft -= filteredProvider.matchPatterns.size;
}
return lineItems;
};

View File

@@ -0,0 +1,104 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Bookmark} from '../types';
const FLIPPER_NAVIGATION_PLUGIN_DB = 'flipper_navigation_plugin_db';
const FLIPPER_NAVIGATION_PLUGIN_DB_VERSION = 1;
const BOOKMARKS_KEY = 'bookmarks';
const createBookmarksObjectStore = (db: IDBDatabase) => {
return new Promise<void>((resolve, reject) => {
if (!db.objectStoreNames.contains(BOOKMARKS_KEY)) {
const bookmarksObjectStore = db.createObjectStore(BOOKMARKS_KEY, {
keyPath: 'uri',
});
bookmarksObjectStore.transaction.oncomplete = () => resolve();
bookmarksObjectStore.transaction.onerror = () =>
reject(bookmarksObjectStore.transaction.error);
} else {
resolve();
}
});
};
const initializeNavigationPluginDB = (db: IDBDatabase) => {
return Promise.all([createBookmarksObjectStore(db)]);
};
const openNavigationPluginDB: () => Promise<IDBDatabase> = () => {
return new Promise((resolve, reject) => {
const openRequest = window.indexedDB.open(
FLIPPER_NAVIGATION_PLUGIN_DB,
FLIPPER_NAVIGATION_PLUGIN_DB_VERSION,
);
openRequest.onupgradeneeded = () => {
const db = openRequest.result;
initializeNavigationPluginDB(db).then(() => resolve(db));
};
openRequest.onerror = () => reject(openRequest.error);
openRequest.onsuccess = () => resolve(openRequest.result);
});
};
export const writeBookmarkToDB = (bookmark: Bookmark) => {
return new Promise<void>((resolve, reject) => {
openNavigationPluginDB()
.then((db: IDBDatabase) => {
const bookmarksObjectStore = db
.transaction(BOOKMARKS_KEY, 'readwrite')
.objectStore(BOOKMARKS_KEY);
const request = bookmarksObjectStore.put(bookmark);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
})
.catch(reject);
});
};
export const readBookmarksFromDB: () => Promise<Map<string, Bookmark>> = () => {
return new Promise((resolve, reject) => {
const bookmarks = new Map();
openNavigationPluginDB()
.then((db: IDBDatabase) => {
const bookmarksObjectStore = db
.transaction(BOOKMARKS_KEY)
.objectStore(BOOKMARKS_KEY);
const request = bookmarksObjectStore.openCursor();
request.onsuccess = () => {
const cursor = request.result;
if (cursor) {
const bookmark = cursor.value;
bookmarks.set(bookmark.uri, bookmark);
cursor.continue();
} else {
resolve(bookmarks);
}
};
request.onerror = () => reject(request.error);
})
.catch(reject);
});
};
export const removeBookmarkFromDB: (uri: string) => Promise<void> = (uri) => {
return new Promise<void>((resolve, reject) => {
openNavigationPluginDB()
.then((db: IDBDatabase) => {
const bookmarksObjectStore = db
.transaction(BOOKMARKS_KEY, 'readwrite')
.objectStore(BOOKMARKS_KEY);
const request = bookmarksObjectStore.delete(uri);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
})
.catch(reject);
});
};

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import querystring from 'querystring';
export const validateParameter = (value: string, parameter: string) => {
return (
value &&
(parameterIsNumberType(parameter) ? !isNaN(parseInt(value, 10)) : true) &&
(parameterIsBooleanType(parameter)
? value === 'true' || value === 'false'
: true)
);
};
export const filterOptionalParameters = (uri: string) => {
return uri.replace(/[/&]?([^&?={}\/]*=)?{\?.*?}/g, '');
};
export const parseURIParameters = (query: string) => {
// get parameters from query string and store in Map
const parameters = query.split('?').splice(1).join('');
const parametersObj = querystring.parse(parameters);
const parametersMap = new Map<string, string>();
for (const key in parametersObj) {
parametersMap.set(key, parametersObj[key] as string);
}
return parametersMap;
};
export const parameterIsNumberType = (parameter: string) => {
const regExp = /^{(#|\?#)/g;
return regExp.test(parameter);
};
export const parameterIsBooleanType = (parameter: string) => {
const regExp = /^{(!|\?!)/g;
return regExp.test(parameter);
};
export const replaceRequiredParametersWithValues = (
uri: string,
values: Array<string>,
) => {
const parameterRegExp = /{[^?]*?}/g;
const replaceRegExp = /{[^?]*?}/;
let newURI = uri;
let index = 0;
let match = parameterRegExp.exec(uri);
while (match != null) {
newURI = newURI.replace(replaceRegExp, values[index]);
match = parameterRegExp.exec(uri);
index++;
}
return newURI;
};
export const getRequiredParameters = (uri: string) => {
const parameterRegExp = /{[^?]*?}/g;
const matches: Array<string> = [];
let match = parameterRegExp.exec(uri);
while (match != null) {
if (match[0]) {
matches.push(match[0]);
}
match = parameterRegExp.exec(uri);
}
return matches;
};
export const liveEdit = (uri: string, formValues: Array<string>) => {
const parameterRegExp = /({[^?]*?})/g;
const uriArray = uri.split(parameterRegExp);
return uriArray.reduce((acc, uriComponent, idx) => {
if (idx % 2 === 0 || !formValues[(idx - 1) / 2]) {
return acc + uriComponent;
} else {
return acc + formValues[(idx - 1) / 2];
}
});
};
export const stripQueryParameters = (uri: string) => {
return uri.replace(/\?.*$/g, '');
};