Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ To see documents, add one or more documents and push a new app version to rebuil

Finally, you should also be able to launch the Firebase UI using the link in the VSCode Launch db window to see the current database state directly.

# Troubleshooting

## Port Taken/Not Available

Occasionally, a process will fail to fully shut down, causing problems when you next attempt to `Launch servers` since the port is already taken.
If a process fails to start because a port is already taken, you can kill the process squatting on the port by running `lsof -i :<port number>`, e.g., `lsof -i :8080`, to get the PID of the process.
You can then kill the process using `kill <PID>` (or, possibly, `kill -9 <PID>`).

# Deploying To Production

To allow the App to connect to Firestore, the default compute service account must be given the Cloud Datastore User role in IAM.
Expand Down
1,586 changes: 951 additions & 635 deletions frontend/package-lock.json

Large diffs are not rendered by default.

18 changes: 6 additions & 12 deletions frontend/src/app/document-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { filterElements, SortOrder } from "../search/filter";
import { DocumentContextMenu } from "../cards/document-card";
import { ElementCard } from "../cards/element-card";
import { ContextMenuButton } from "../cards/card-components";
import { FilterCallout } from "../search/filter-callout";
import { SearchCallout } from "../search/search-errors";
import {
AppErrorState,
AppInternalErrorState,
Expand Down Expand Up @@ -174,17 +174,7 @@ export function DocumentListContent(props: DocumentListCardsProps): ReactNode {
icon="warning-sign"
iconIntent="warning"
title="All elements are hidden by filters"
action={<ClearFiltersButton standardSize />}
/>
);
}

let callout = null;
if (filterResult.filtered > 0) {
callout = (
<FilterCallout
objectLabel="elements"
filtered={filterResult.filtered}
action={<ClearFiltersButton />}
/>
);
}
Expand All @@ -193,6 +183,10 @@ export function DocumentListContent(props: DocumentListCardsProps): ReactNode {
<ElementCard key={element.id} element={element} />
));

const callout = (
<SearchCallout objectLabel="element" filtered={filterResult.filtered} />
);

return (
<>
{callout}
Expand Down
90 changes: 36 additions & 54 deletions frontend/src/favorites/favorites-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,16 @@ import { ElementObj } from "../api/models";
import { useUiState } from "../api/ui-state";
import {
AppInternalErrorState,
AppLoadingState,
AppErrorState
AppLoadingState
} from "../common/app-zero-state";
import { FilterCallout } from "../search/filter-callout";
import { ClearFiltersButton } from "../navbar/vendor-filters";
import { NoSearchResultError, SearchCallout } from "../search/search-errors";
import { FavoriteCard } from "./favorite-card";
import {
useLibraryQuery,
useLibraryUserDataQuery,
useSearchDbQuery
} from "../queries";
import { doSearch, SearchHit } from "../search/search";
import { NoSearchResultError } from "../search/search-results";
import { doSearch, FilterResult, SearchHit } from "../search/search";

/**
* A list of current favorite cards.
Expand Down Expand Up @@ -63,17 +60,8 @@ export function FavoritesList(): ReactNode {
.map((favorite) => elements[favorite.id])
.filter((element) => !!element);

if (favoriteElements.length == 0) {
return (
<AppErrorState
title="No favorites"
icon="heart-broken"
iconColor={Colors.RED3}
/>
);
}

let filterResult;
let filteredElements: ElementObj[];
let filterResult: FilterResult;
let searchHits: Record<string, SearchHit> = {};
if (uiState.searchQuery) {
if (!searchDbQuery.data) {
Expand All @@ -87,64 +75,58 @@ export function FavoritesList(): ReactNode {
{
vendors: uiState.vendorFilters,
isFavorite: true
}
},
userData.favorites
);

if (searchResults.hits.length === 0) {
return (
<NoSearchResultError
objectLabel="favorites"
filtered={searchResults.filtered}
/>
);
}
// Search hits are just ids and positions, so convert back into array of elements
filteredElements = searchResults.hits
.map((hit) => elements[hit.id])
.filter((element) => !!element);

filterResult = {
elements: searchResults.hits
.map((hit) => elements[hit.id])
.filter((element) => !!element),
filtered: searchResults.filtered
};
filterResult = searchResults.filtered;

searchHits = searchResults.hits.reduce((acc, hit) => {
acc[hit.id] = hit;
return acc;
// To get the search hits later build a map of actual hits as well
searchHits = searchResults.hits.reduce((searchHits, hit) => {
searchHits[hit.id] = hit;
return searchHits;
}, {} as Record<string, SearchHit>);
} else {
filterResult = filterElements(favoriteElements, {
const filterElementResult = filterElements(favoriteElements, {
vendors: uiState.vendorFilters,
// Only elements which haven't been disabled can be shown
isVisible: true
});

if (filterResult.elements.length == 0) {
return (
<AppErrorState
title="All favorites are hidden by filters"
icon="heart-broken"
iconColor={Colors.RED3}
action={<ClearFiltersButton standardSize />}
/>
);
}
filteredElements = filterElementResult.elements;
filterResult = filterElementResult.filtered;
}

if (filteredElements.length === 0) {
return (
<NoSearchResultError
objectLabel="favorite"
filtered={filterResult}
/>
);
}

let callout;
if (filterResult.filtered > 0) {
let callout = null;
// Favorites specifically are displayed inline inside a ListContainer, so we need to wrap a custom Card around it
// So we can't rely on SearchCallout returning null :(
if (filterResult.byDocument > 0 || filterResult.byVendor > 0) {
callout = (
<Card className="item-card" style={{ padding: "0px" }}>
<FilterCallout
objectLabel="favorites"
filtered={filterResult.filtered}
/>
<SearchCallout objectLabel="favorite" filtered={filterResult} />
</Card>
);
}

const cards = filterResult.elements.map((element: ElementObj) => {
const cards = filteredElements.map((element: ElementObj) => {
// Fetch the favorite again so we have it's data
const favorite = userData.favorites[element.id];
if (!favorite) {
return null;
return null; // Shouldn't happen
}
return (
<FavoriteCard
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ $frc-design-green: #4cae4f;
}
}

.search-bar {
max-width: 200px;
.search-input {
// flex-shrink: 1;
// min-width: 50px;
}
}

.app-content {
overflow-y: auto;
flex-grow: 1;
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/navbar/app-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,27 @@ export function SearchBar() {
const ref = useRef<HTMLInputElement>(null);
const [uiState, setUiState] = useUiState();

const clearButton = uiState.searchQuery ? (
<Button
variant="minimal"
icon="cross-circle"
onClick={() => {
if (ref.current) {
ref.current.value = "";
}
setUiState({ searchQuery: undefined });
}}
size="small"
/>
) : undefined;

return (
<InputGroup
type="search"
leftIcon="search"
placeholder="Search library..."
className="search-bar"
inputClassName="search-input"
inputRef={ref}
value={uiState.searchQuery}
onFocus={() => {
Expand All @@ -118,6 +134,7 @@ export function SearchBar() {
const query = value === "" ? undefined : value;
setUiState({ searchQuery: query });
}}
rightElement={clearButton}
/>
);
}
11 changes: 7 additions & 4 deletions frontend/src/navbar/vendor-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ interface ClearFiltersButtonProps {
* @default "Clear filters"
*/
text?: string;
standardSize?: boolean;
/**
* @default false
*/
small?: boolean;
}

export function ClearFiltersButton(props: ClearFiltersButtonProps): ReactNode {
const [uiState, setUiState] = useUiState();
const text = props.text ?? "Clear filters";
const standardSize = props.standardSize ?? false;
const small = props.small ?? false;

const vendorFilters = uiState.vendorFilters;
const areAllTagsActive = vendorFilters === undefined;
Expand All @@ -25,7 +28,7 @@ export function ClearFiltersButton(props: ClearFiltersButtonProps): ReactNode {
text={text}
disabled={areAllTagsActive}
variant={ButtonVariant.OUTLINED}
size={standardSize ? Size.MEDIUM : Size.SMALL}
size={small ? Size.SMALL : Size.MEDIUM}
icon="filter-remove"
onClick={() => {
setUiState({ vendorFilters: undefined });
Expand Down Expand Up @@ -65,7 +68,7 @@ export function VendorFilters(): ReactNode {
return (
<div className="split" style={{ gap: "5x" }}>
<div className="vendor-filter-tags">{filterTags}</div>
<ClearFiltersButton text="Clear" />
<ClearFiltersButton text="Clear" small />
</div>
);
}
Expand Down
25 changes: 0 additions & 25 deletions frontend/src/search/filter-callout.tsx

This file was deleted.

24 changes: 15 additions & 9 deletions frontend/src/search/filter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ElementObj, Vendor } from "../api/models";
import { FilterResult } from "./search";

export enum SortOrder {
DEFAULT = "default",
Expand All @@ -22,21 +23,26 @@ export interface FilterArgs {
isVisible?: boolean;
}

export interface FilterResult {
interface VendorFilterResult extends FilterResult {
byDocument: 0;
}

/**
* A list of elements which have (possibly) been filtered down.
*/
export interface FilteredElements {
elements: ElementObj[];
/**
* The number of entities that were filtered out by user-controllable filters.
*/
filtered: number;
filtered: VendorFilterResult;
}

/**
* Returns an ordered list of elements in a document and tracks how many were filtered by vendors.
* Does not include handling for being in a document since this should only be used when search is not active.
*/
export function filterElements(
elements: ElementObj[],
args: FilterArgs
): FilterResult {
): FilteredElements {
let filteredElements = [...elements];

// Filter by visibility
Expand All @@ -47,7 +53,7 @@ export function filterElements(
}

// Filter by vendors and track how many were removed
let filtered = 0;
let filteredByVendor = 0;
if (args.vendors && args.vendors.length > 0) {
const vendorSet = new Set(args.vendors);
const beforeCount = filteredElements.length;
Expand All @@ -56,7 +62,7 @@ export function filterElements(
element.vendors &&
element.vendors.some((vendor) => vendorSet.has(vendor))
);
filtered = beforeCount - filteredElements.length;
filteredByVendor = beforeCount - filteredElements.length;
}

// Sorting
Expand All @@ -69,6 +75,6 @@ export function filterElements(

return {
elements: filteredElements,
filtered
filtered: { byDocument: 0, byVendor: filteredByVendor }
};
}
Loading