Refactor plugin to make it fast refreshable

Summary: Refactored Navigation plugin to make it fast-refreshable: moved the main component into a separate file and exported all components as named functions. Without these changes every change of UI triggered full reload.

Reviewed By: timur-valiev

Differential Revision: D29814077

fbshipit-source-id: 5285bdc5f14a5163f9501c0d45a3affefb08fc8e
This commit is contained in:
Anton Nikolaev
2021-07-21 07:23:48 -07:00
committed by Facebook GitHub Bot
parent a78b6124d7
commit d782f19001
13 changed files with 295 additions and 275 deletions

View File

@@ -0,0 +1,78 @@
/**
* 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 {
BookmarksSidebar,
SaveBookmarkDialog,
SearchBar,
Timeline,
} from './components';
import {
appMatchPatternsToAutoCompleteProvider,
bookmarksToAutoCompleteProvider,
} from './util/autoCompleteProvider';
import React, {useMemo} from 'react';
import {useValue, usePlugin, Layout} from 'flipper-plugin';
import {plugin} from './plugin';
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 grow>
<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

@@ -50,7 +50,7 @@ const SheetItemIcon = styled.span({
padding: 8, padding: 8,
}); });
export default (props: Props) => { export function AutoCompleteSheet(props: Props) {
const {providers, onHighlighted, onNavigate, query} = props; const {providers, onHighlighted, onNavigate, query} = props;
const lineItems = filterProvidersToLineItems(providers, query, MAX_ITEMS); const lineItems = filterProvidersToLineItems(providers, query, MAX_ITEMS);
lineItems.unshift({uri: query, matchPattern: query, icon: 'send'}); lineItems.unshift({uri: query, matchPattern: query, icon: 'send'});
@@ -70,4 +70,4 @@ export default (props: Props) => {
))} ))}
</AutoCompleteSheetContainer> </AutoCompleteSheetContainer>
); );
}; }

View File

@@ -78,7 +78,7 @@ const alphabetizeBookmarkCompare = (b1: Bookmark, b2: Bookmark) => {
return b1.uri < b2.uri ? -1 : b1.uri > b2.uri ? 1 : 0; return b1.uri < b2.uri ? -1 : b1.uri > b2.uri ? 1 : 0;
}; };
export default (props: Props) => { export function BookmarksSidebar(props: Props) {
const {bookmarks, onNavigate, onRemove} = props; const {bookmarks, onNavigate, onRemove} = props;
return ( return (
<DetailSidebar> <DetailSidebar>
@@ -119,4 +119,4 @@ export default (props: Props) => {
</Panel> </Panel>
</DetailSidebar> </DetailSidebar>
); );
}; }

View File

@@ -27,7 +27,7 @@ const FavoriteButtonContainer = styled.div({
}, },
}); });
export default (props: Props) => { export function FavoriteButton(props: Props) {
const {highlighted, onClick, ...iconButtonProps} = props; const {highlighted, onClick, ...iconButtonProps} = props;
return ( return (
<FavoriteButtonContainer> <FavoriteButtonContainer>
@@ -42,4 +42,4 @@ export default (props: Props) => {
<IconButton outline icon="star" onClick={onClick} {...iconButtonProps} /> <IconButton outline icon="star" onClick={onClick} {...iconButtonProps} />
</FavoriteButtonContainer> </FavoriteButtonContainer>
); );
}; }

View File

@@ -43,23 +43,23 @@ const RippleEffect = styled.div({
}, },
}); });
const IconButton = styled.div({ const IconButtonContainer = styled.div({
':active': { ':active': {
animation: `${shrinkAnimation} .25s ease forwards`, animation: `${shrinkAnimation} .25s ease forwards`,
}, },
}); });
export default function (props: Props) { export function IconButton(props: Props) {
return ( return (
<RippleEffect> <RippleEffect>
<IconButton className="icon-button" onClick={props.onClick}> <IconButtonContainer className="icon-button" onClick={props.onClick}>
<Glyph <Glyph
name={props.icon} name={props.icon}
size={props.size} size={props.size}
color={props.color} color={props.color}
variant={props.outline ? 'outline' : 'filled'} variant={props.outline ? 'outline' : 'filled'}
/> />
</IconButton> </IconButtonContainer>
</RippleEffect> </RippleEffect>
); );
} }

View File

@@ -160,7 +160,7 @@ const buildParameterTable = (parameters: Map<string, string>) => {
); );
}; };
export default (props: Props) => { export function NavigationInfoBox(props: Props) {
const { const {
uri, uri,
isBookmarked, isBookmarked,
@@ -238,4 +238,4 @@ export default (props: Props) => {
</NavigationInfoBoxContainer> </NavigationInfoBoxContainer>
); );
} }
}; }

View File

@@ -28,7 +28,7 @@ type Props = {
onSubmit: (uri: URI) => void; onSubmit: (uri: URI) => void;
}; };
export default (props: Props) => { export function RequiredParametersDialog(props: Props) {
const {onHide, onSubmit, uri, requiredParameters} = props; const {onHide, onSubmit, uri, requiredParameters} = props;
const {isValid, values, setValuesArray} = const {isValid, values, setValuesArray} =
useRequiredParameterFormValidator(requiredParameters); useRequiredParameterFormValidator(requiredParameters);
@@ -95,4 +95,4 @@ export default (props: Props) => {
</Layout.Container> </Layout.Container>
</Modal> </Modal>
); );
}; }

View File

@@ -48,7 +48,7 @@ const NameInput = styled(Input)({
height: 30, height: 30,
}); });
export default (props: Props) => { export function SaveBookmarkDialog(props: Props) {
const {edit, shouldShow, onHide, onRemove, onSubmit, uri} = props; const {edit, shouldShow, onHide, onRemove, onSubmit, uri} = props;
const [commonName, setCommonName] = useState(''); const [commonName, setCommonName] = useState('');
if (uri == null || !shouldShow) { if (uri == null || !shouldShow) {
@@ -114,4 +114,4 @@ export default (props: Props) => {
</Sheet> </Sheet>
); );
} }
}; }

View File

@@ -57,7 +57,7 @@ const SearchInputContainer = styled.div({
position: 'relative', position: 'relative',
}); });
class SearchBar extends Component<Props, State> { export class SearchBar extends Component<Props, State> {
state = { state = {
inputFocused: false, inputFocused: false,
autoCompleteSheetOpen: false, autoCompleteSheetOpen: false,
@@ -158,5 +158,3 @@ class SearchBar extends Component<Props, State> {
); );
} }
} }
export default SearchBar;

View File

@@ -58,7 +58,7 @@ const NoData = styled(FlexCenter)({
color: theme.textColorSecondary, color: theme.textColorSecondary,
}); });
export default (props: Props) => { export function Timeline(props: Props) {
const {bookmarks, events, onNavigate, onFavorite} = props; const {bookmarks, events, onNavigate, onFavorite} = props;
const timelineRef = useRef<HTMLDivElement>(null); const timelineRef = useRef<HTMLDivElement>(null);
return events.length === 0 ? ( return events.length === 0 ? (
@@ -92,4 +92,4 @@ export default (props: Props) => {
</div> </div>
</TimelineContainer> </TimelineContainer>
); );
}; }

View File

@@ -7,12 +7,12 @@
* @format * @format
*/ */
export {default as AutoCompleteSheet} from './AutoCompleteSheet'; export {AutoCompleteSheet} from './AutoCompleteSheet';
export {default as BookmarksSidebar} from './BookmarksSidebar'; export {BookmarksSidebar} from './BookmarksSidebar';
export {default as FavoriteButton} from './FavoriteButton'; export {FavoriteButton} from './FavoriteButton';
export {default as IconButton} from './IconButton'; export {IconButton} from './IconButton';
export {default as NavigationInfoBox} from './NavigationInfoBox'; export {NavigationInfoBox} from './NavigationInfoBox';
export {default as RequiredParametersDialog} from './RequiredParametersDialog'; export {RequiredParametersDialog} from './RequiredParametersDialog';
export {default as SaveBookmarkDialog} from './SaveBookmarkDialog'; export {SaveBookmarkDialog} from './SaveBookmarkDialog';
export {default as SearchBar} from './SearchBar'; export {SearchBar} from './SearchBar';
export {default as Timeline} from './Timeline'; export {Timeline} from './Timeline';

View File

@@ -8,248 +8,5 @@
* @flow strict-local * @flow strict-local
*/ */
import {bufferToBlob} from 'flipper'; export {plugin, NavigationPlugin} from './plugin';
import { export {Component} from './Component';
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,187 @@
/**
* 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 {RequiredParametersDialog} from './components';
import {
removeBookmarkFromDB,
readBookmarksFromDB,
writeBookmarkToDB,
} from './util/indexedDB';
import {} from './util/autoCompleteProvider';
import {getAppMatchPatterns} from './util/appMatchPatterns';
import {getRequiredParameters, filterOptionalParameters} from './util/uri';
import {
Bookmark,
NavigationEvent,
AppMatchPattern,
URI,
RawNavigationEvent,
} from './types';
import React from 'react';
import {PluginClient, createState, 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;
},
};
}
/* @scarf-info: do not remove, more info: https://fburl.com/scarf */
/* @scarf-generated: flipper-plugin index.js.template 0bfa32e5-fb15-4705-81f8-86260a1f3f8e */