Skip to content

Commit ce5883e

Browse files
Merge pull request #418 from iFixit/hide-repos
FilterMenu: support a "defaultExcludedValues" prop
2 parents 4f09b1f + 4926059 commit ce5883e

7 files changed

Lines changed: 109 additions & 27 deletions

File tree

config.example.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ module.exports = {
7575
name: "owner/otherRepo",
7676
requiredStatuses: ["tests", "build", "codeClimate"],
7777
ignoredStatuses: ["coverage"],
78+
// Hides pulls on this repo on page load, users can unhide them with the repo filter
79+
hideByDefault: true,
7880
},
7981
],
8082

frontend/src/filter-menu.tsx

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,37 @@ import {
1010
MenuDivider,
1111
MenuOptionGroup,
1212
MenuItemOption,
13+
chakra,
1314
} from "@chakra-ui/react";
1415
import { useEffect, useMemo } from "react";
1516
import { countBy } from "lodash-es";
1617

1718
// Map from value to number of pulls that have that value
1819
type ValueGetter = (pull: Pull) => string;
1920

21+
const SHOWALL = "SHOWALL";
22+
2023
type FilterMenuProps = {
2124
urlParam: string;
2225
buttonText: string;
2326
extractValueFromPull: ValueGetter;
27+
defaultExculdedValues?: string[];
2428
};
2529

2630
export function FilterMenu({
2731
urlParam,
2832
buttonText,
2933
extractValueFromPull,
34+
defaultExculdedValues,
3035
}: FilterMenuProps) {
3136
const pulls = useAllOpenPulls();
3237
// Default is empty array that implies show all pulls (no filtering)
3338
const [selectedValues, setSelectedValues] = useArrayUrlState(urlParam, []);
34-
// Nothing selected == show everything, otherwise, it'd be empty
35-
const showAll = selectedValues.length === 0;
39+
// Nothing selected == show the default values (everything except the excluded values)
40+
const showDefault = selectedValues.length === 0;
41+
// Show every single value if the magic SHOWALL string is selected
42+
const showAll = notEmpty(defaultExculdedValues) ? selectedValues.includes(SHOWALL) : selectedValues.length === 0;
43+
3644
// List from url may contain values we have no pulls for
3745
const urlValues = useConst(() => new Set(selectedValues));
3846
const setPullFilter = useSetFilter();
@@ -41,50 +49,87 @@ export function FilterMenu({
4149
const allValues = useMemo(() => {
4250
// All values of open pulls
4351
const pullValues = new Set<string>(pulls.map(extractValueFromPull));
44-
return sortValues([...new Set([...pullValues, ...urlValues])]);
52+
const allValuesSet = new Set([...pullValues, ...urlValues]);
53+
allValuesSet.delete(SHOWALL);
54+
return sortValues([...allValuesSet]);
4555
}, [pulls]);
56+
4657
const valueToPullCount = useMemo(
4758
() => countBy(pulls, extractValueFromPull),
4859
[pulls]
4960
);
5061

62+
const defaultSelectedValues = arrayDiff(allValues, defaultExculdedValues || []);
63+
5164
useEffect(() => {
5265
const selectedValuesSet = new Set(selectedValues);
5366
setPullFilter(
5467
urlParam,
55-
selectedValuesSet.size === 0
68+
showAll
5669
? null
57-
: (pull) => selectedValuesSet.has(extractValueFromPull(pull))
70+
: (showDefault ? (pull) => !defaultExculdedValues?.includes(extractValueFromPull(pull))
71+
: (pull) => selectedValuesSet.has(extractValueFromPull(pull)))
5872
);
59-
}, [selectedValues]);
73+
}, [selectedValues, defaultExculdedValues]);
74+
75+
const numberText = showDefault ? "" : (showAll ? allValues.length : selectedValues.length);
6076

6177
return (
6278
<Menu closeOnSelect={false}>
6379
<MenuButton
6480
as={Button}
6581
colorScheme="blue"
6682
size="sm"
67-
variant={showAll ? "outline" : null}
83+
variant={showDefault ? "outline" : null}
6884
>
69-
{buttonText} {selectedValues.length ? `(${selectedValues.length})` : ""}
85+
{buttonText} {numberText ? `(${numberText})` : ""}
7086
</MenuButton>
7187
<MenuList minWidth="240px">
72-
<MenuItemOption
73-
key="Show All"
74-
onClick={() => setSelectedValues([])}
75-
isChecked={showAll}
76-
>
77-
Show All
78-
</MenuItemOption>
88+
{notEmpty(defaultExculdedValues) && (
89+
<>
90+
<MenuItemOption
91+
key="Show All"
92+
onClick={() => setSelectedValues([SHOWALL])}
93+
>
94+
Show All
95+
</MenuItemOption>
96+
<MenuItemOption
97+
key="Show Default"
98+
onClick={() => setSelectedValues([])}
99+
>
100+
Show Default
101+
</MenuItemOption>
102+
</>)
103+
}
104+
{empty(defaultExculdedValues) &&
105+
<MenuItemOption
106+
key="Show All"
107+
onClick={() => setSelectedValues([])}
108+
>
109+
Show All
110+
</MenuItemOption>
111+
}
79112
<MenuDivider />
80113
<MenuOptionGroup
81114
type="checkbox"
82-
value={showAll ? [] : selectedValues}
115+
value={showAll ? allValues : (showDefault ? defaultSelectedValues : selectedValues)}
83116
onChange={setSelectedValues}
84117
>
85118
{allValues.map((value) => (
86-
<MenuItemOption key={value} value={value}>
119+
<MenuItemOption className="filterOption" key={value} value={value}>
87120
{value} ({valueToPullCount[value] || 0})
121+
<chakra.span
122+
visibility="hidden"
123+
float="right"
124+
_hover={{textDecoration:"underline"}}
125+
mt={1}
126+
fontSize="xs"
127+
sx={{'.filterOption:hover &': {visibility: 'visible'}}}
128+
onClick={(e)=> {
129+
setSelectedValues([value]);
130+
e.stopPropagation();
131+
}
132+
}>only</chakra.span>
88133
</MenuItemOption>
89134
))}
90135
</MenuOptionGroup>
@@ -98,3 +143,15 @@ function sortValues(values: string[]): string[] {
98143
a.localeCompare(b, undefined, { sensitivity: "base" })
99144
);
100145
}
146+
147+
function arrayDiff<T>(a: T[], b: T[]): T[] {
148+
return a.filter((x) => !b.includes(x));
149+
}
150+
151+
function empty<T>(array: T[] | undefined): boolean {
152+
return !array || array.length === 0;
153+
}
154+
155+
function notEmpty<T>(array: T[] | undefined): boolean {
156+
return !!array && array.length > 0;
157+
}

frontend/src/navbar.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
useFilteredOpenPulls,
55
useAllOpenPulls,
66
useSetFilter,
7+
useRepoSpecs,
78
} from "./pulldasher/pulls-context";
89
import { Pull } from "./pull";
910
import {
@@ -21,7 +22,7 @@ import {
2122
MenuList,
2223
Text
2324
} from "@chakra-ui/react";
24-
import { useRef, useEffect, useCallback } from "react";
25+
import { useRef, useEffect, useCallback, useMemo } from "react";
2526
import { useBoolUrlState } from "./use-url-state";
2627
import { NotificationRequest } from "./notifications";
2728
import { useConnectionState, ConnectionState } from "./backend/socket";
@@ -51,6 +52,10 @@ export function Navbar(props: NavBarProps) {
5152
const pulls: Set<Pull> = useFilteredOpenPulls();
5253
const allOpenPulls: Pull[] = useAllOpenPulls();
5354
const setPullFilter = useSetFilter();
55+
const repoSpecs = useRepoSpecs();
56+
const reposToHide = useMemo(() =>
57+
repoSpecs.filter((repo) => repo.hideByDefault).map((repo) => repo.name.replace(/.*\//g, "")),
58+
[repoSpecs]);
5459
const { toggleColorMode } = useColorMode();
5560
const [showCryo, toggleShowCryo] = useBoolUrlState("cryo", false);
5661
const [showExtBlocked, toggleShowExtBlocked] = useBoolUrlState(
@@ -174,6 +179,7 @@ export function Navbar(props: NavBarProps) {
174179
<FilterMenu
175180
urlParam="repo"
176181
buttonText="Repo"
182+
defaultExculdedValues={reposToHide}
177183
extractValueFromPull={(pull: Pull) => pull.getRepoName()}
178184
/>
179185
</Box>

frontend/src/pull-card/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,9 @@ const formatDate = (dateStr: string | null) => {
196196
return dateStr ? formatter.format(new Date(dateStr)) : null;
197197
};
198198

199-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
200199
function highlightOnChange(
201200
ref: RefObject<HTMLElement>,
201+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
202202
dependencies: Array<any>
203203
) {
204204
// Animate a highlight when pull.received_at changes

frontend/src/pulldasher/pulls-context.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "./filtered-pulls-state";
77
import { usePullsState } from "./pulls-state";
88
import { Pull } from "../pull";
9+
import { RepoSpec } from "../types";
910
import { defaultCompare } from "./sort";
1011

1112
interface PullContextProps {
@@ -19,6 +20,8 @@ interface PullContextProps {
1920
filteredOpenPulls: Set<Pull>;
2021
// Changes the filter function
2122
setFilter: FilterFunctionSetter;
23+
// RepoSpecs (.repos) from the config
24+
repoSpecs: RepoSpec[];
2225
}
2326

2427
const defaultProps = {
@@ -29,6 +32,7 @@ const defaultProps = {
2932
// Default implementation is a no-op, just so there's
3033
// something there until the provider is used
3134
setFilter: (name: string, filter: FilterFunction) => filter,
35+
repoSpecs: [],
3236
};
3337
const PullsContext = createContext<PullContextProps>(defaultProps);
3438

@@ -52,12 +56,16 @@ export function useSetFilter(): FilterFunctionSetter {
5256
return useContext(PullsContext).setFilter;
5357
}
5458

59+
export function useRepoSpecs(): RepoSpec[] {
60+
return useContext(PullsContext).repoSpecs;
61+
}
62+
5563
export const PullsProvider = function ({
5664
children,
5765
}: {
5866
children: React.ReactNode;
5967
}) {
60-
const unfilteredPulls = usePullsState();
68+
const {pullState: unfilteredPulls, repoSpecs} = usePullsState();
6169
const [filteredPulls, setFilter] = useFilteredPullsState(unfilteredPulls);
6270
const openPulls = unfilteredPulls.filter(isOpen);
6371
const contextValue = {
@@ -66,6 +74,7 @@ export const PullsProvider = function ({
6674
filteredPulls: filteredPulls,
6775
allPulls: unfilteredPulls,
6876
setFilter,
77+
repoSpecs,
6978
};
7079
return (
7180
<PullsContext.Provider value={contextValue}>

frontend/src/pulldasher/pulls-state.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@ import { createPullSocket } from "../backend/pull-socket";
44
import { PullData, RepoSpec } from "../types";
55
import { Pull } from "../pull";
66

7-
function onPullsChanged(pullsChanged: (pulls: Pull[]) => void) {
7+
function onPullsChanged(pullsChanged: (pulls: Pull[], repoSpecs: RepoSpec[]) => void) {
88
const pulls: Record<string, Pull> = {};
9-
const pullRefresh = () => pullsChanged(Object.values(pulls));
9+
const pullRefresh = () => pullsChanged(Object.values(pulls), repoSpecs);
1010
const throttledPullRefresh: () => void = throttle(pullRefresh, 500);
11+
let repoSpecs: RepoSpec[] = [];
1112

12-
createPullSocket((pullDatas: PullData[], repoSpecs: RepoSpec[]) => {
13+
createPullSocket((pullDatas: PullData[], newRepoSpecs: RepoSpec[]) => {
1314
pullDatas.forEach((pullData: PullData) => {
1415
pullData.repoSpec =
15-
repoSpecs.find((repo) => repo.name == pullData.repo) || null;
16+
newRepoSpecs.find((repo) => repo.name == pullData.repo) || null;
1617
pullData.received_at = new Date();
1718
const pull: Pull = new Pull(pullData);
1819
pulls[pull.getKey()] = pull;
1920
});
21+
22+
repoSpecs = newRepoSpecs || [];
2023
throttledPullRefresh();
2124
});
2225
}
@@ -25,16 +28,20 @@ function onPullsChanged(pullsChanged: (pulls: Pull[]) => void) {
2528
* Note: This is only meant to be used in one component
2629
*/
2730
let socketInitialized = false;
28-
export function usePullsState(): Pull[] {
31+
export function usePullsState() {
2932
const [pullState, setPullsState] = useState<Pull[]>([]);
33+
const [repoSpecs, setRepoSpecs] = useState<RepoSpec[]>([]);
3034
useEffect(() => {
3135
if (socketInitialized) {
3236
throw new Error(
3337
"usePullsState() connects to socket-io and is only meant to be used in the PullsProvider component, see useFilteredOpenPulls() instead."
3438
);
3539
}
3640
socketInitialized = true;
37-
onPullsChanged(setPullsState);
41+
onPullsChanged((pulls, repoSpecs) => {
42+
setPullsState(pulls);
43+
setRepoSpecs(repoSpecs);
44+
});
3845
}, []);
39-
return pullState;
46+
return {pullState, repoSpecs};
4047
}

frontend/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface SignatureUser {
3232
export interface RepoSpec {
3333
requiredStatuses?: string[];
3434
ignoredStatuses?: string[];
35+
hideByDefault?: boolean;
3536
name: string;
3637
}
3738

0 commit comments

Comments
 (0)