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:
committed by
Facebook GitHub Bot
parent
32bf4c32c2
commit
b3274a8450
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
65
desktop/plugins/public/navigation/components/IconButton.tsx
Normal file
65
desktop/plugins/public/navigation/components/IconButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
166
desktop/plugins/public/navigation/components/SearchBar.tsx
Normal file
166
desktop/plugins/public/navigation/components/SearchBar.tsx
Normal 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;
|
||||
94
desktop/plugins/public/navigation/components/Timeline.tsx
Normal file
94
desktop/plugins/public/navigation/components/Timeline.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
18
desktop/plugins/public/navigation/components/index.tsx
Normal file
18
desktop/plugins/public/navigation/components/index.tsx
Normal 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';
|
||||
Reference in New Issue
Block a user