Skip to content

Commit dd48251

Browse files
committed
feat: Refactoring home view filters from sidebar to horizontal element.
1 parent c16bf49 commit dd48251

33 files changed

+3054
-2836
lines changed

.eslintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ src/react-app-env.d.ts
44
*.test.ts
55
*.test.tsx
66
*.spec.ts
7-
src/**/__tests__/*
7+
src/**/__tests__/*
8+
src/mocks/*

.yarnrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodeLinker: node-modules

src/assets/chevron.svg

Lines changed: 5 additions & 4 deletions
Loading

src/assets/trash.svg

Lines changed: 5 additions & 0 deletions
Loading

src/components/AppBar/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ const Wrapper = styled.header`
4949
left: 0;
5050
right: 0;
5151
min-height: var(--layout-application-bar-height);
52+
padding: var(--layout-application-bar-padding);
5253
margin: 0 auto;
53-
padding: var(--layout-page-padding-y) var(--layout-page-padding-x);
5454
background: var(--color-bg-primary);
5555
z-index: 999;
5656
flex-direction: column;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { useState } from 'react';
2+
import styled from 'styled-components';
3+
import Filter, { FilterProps } from '@components/FilterInput/Filter';
4+
import { FilterOptionRow, FilterPopupTrailing } from '@components/FilterInput/FilterRows';
5+
import InputWrapper from '@components/Form/InputWrapper';
6+
import useAutoComplete, { AutoCompleteItem } from '@hooks/useAutoComplete';
7+
8+
export type InputAutocompleteSettings = {
9+
url: string;
10+
finder?: (item: AutoCompleteItem, input: string) => boolean;
11+
params?: (input: string) => Record<string, string>;
12+
preFetch?: boolean;
13+
};
14+
15+
type Props = {
16+
onSelect: (k: string) => void;
17+
onClear?: () => void;
18+
autoCompleteSettings?: InputAutocompleteSettings;
19+
optionLabelRenderer?: (value: string) => React.ReactNode;
20+
} & Omit<FilterProps, 'content'>;
21+
22+
const AutoCompleteFilter: React.FC<Props> = ({ autoCompleteSettings, onClear, optionLabelRenderer, ...props }) => {
23+
const [inputValue, setInputValue] = useState('');
24+
const { result: autoCompleteResult } = useAutoComplete<string>(
25+
autoCompleteSettings
26+
? {
27+
...autoCompleteSettings,
28+
params: autoCompleteSettings.params ? autoCompleteSettings.params(inputValue) : {},
29+
input: inputValue,
30+
enabled: !!autoCompleteSettings,
31+
}
32+
: { url: '', input: '', enabled: false },
33+
);
34+
35+
return (
36+
<Filter
37+
{...props}
38+
content={({ selected, onClose }) => (
39+
<Content>
40+
<FilterPopupHeading>
41+
<InputWrapper active={true}>
42+
<input type="text" onChange={(event) => setInputValue(event.target.value)} />
43+
</InputWrapper>
44+
</FilterPopupHeading>
45+
46+
{selected.map(
47+
(item) =>
48+
item && (
49+
<FilterOptionRow key={item} onClick={() => props.onSelect(item)} selected={true}>
50+
{optionLabelRenderer ? optionLabelRenderer(item) : item}
51+
</FilterOptionRow>
52+
),
53+
)}
54+
55+
{autoCompleteResult.data
56+
.filter((item) => !selected.includes(item.value))
57+
.map((item) => (
58+
<FilterOptionRow key={item.value} onClick={() => props.onSelect(item.value)} selected={false}>
59+
{optionLabelRenderer ? optionLabelRenderer(item.value) : item.value}
60+
</FilterOptionRow>
61+
))}
62+
63+
{onClear && (
64+
<FilterPopupTrailing
65+
clear={{
66+
onClick: () => {
67+
onClear();
68+
onClose();
69+
},
70+
disabled: selected.length === 0,
71+
}}
72+
></FilterPopupTrailing>
73+
)}
74+
</Content>
75+
)}
76+
/>
77+
);
78+
};
79+
80+
const Content = styled.div`
81+
width: max-content;
82+
`;
83+
84+
const FilterPopupHeading = styled.div`
85+
margin-bottom: 0.75rem;
86+
`;
87+
88+
export default AutoCompleteFilter;

src/components/FilterInput/Filter.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React, { useState } from 'react';
2+
import styled from 'styled-components';
3+
import FilterPopup from '@components/FilterInput/FilterPopup';
4+
import Icon from '../Icon';
5+
6+
export type FilterProps = {
7+
label: string;
8+
labelRenderer?: (label: string, value?: string | null) => React.ReactNode;
9+
onSelect?: (k: string) => void;
10+
value?: string | null;
11+
content: (props: { onSelect?: (k: string) => void; selected: string[]; onClose: () => void }) => React.ReactNode;
12+
'data-testid'?: string;
13+
};
14+
15+
const Filter: React.FC<FilterProps> = ({ label, labelRenderer, value = '', onSelect, content, ...rest }) => {
16+
const [filterWindowOpen, setFilterWindowOpen] = useState(false);
17+
const selected = value?.split(',').filter((item) => item) || [];
18+
19+
const close = () => setFilterWindowOpen(false);
20+
21+
return (
22+
<FilterBase {...rest}>
23+
<FilterButton onClick={() => setFilterWindowOpen(!filterWindowOpen)}>
24+
{labelRenderer ? labelRenderer(label, value) : DefaultLabelRenderer(label, value)}
25+
<FilterIcon>
26+
<Icon name="chevron" size="xs" rotate={filterWindowOpen ? -180 : 0} />
27+
</FilterIcon>
28+
</FilterButton>
29+
{filterWindowOpen && <FilterPopup onClose={close}>{content({ onSelect, selected, onClose: close })}</FilterPopup>}
30+
</FilterBase>
31+
);
32+
};
33+
34+
export const DefaultLabelRenderer = (label: string, value?: string | null) => (
35+
<>
36+
{`${label}${value ? ':' : ''}`}
37+
{value ? <SelectedValue>{value}</SelectedValue> : ''}
38+
</>
39+
);
40+
41+
const FilterBase = styled.div`
42+
position: relative;
43+
white-space: nowrap;
44+
min-width: 1rem;
45+
`;
46+
47+
const FilterButton = styled.div`
48+
padding: 0.375rem 0.5rem;
49+
border: var(--input-border);
50+
border-radius: var(--radius-primary);
51+
color: var(--input-text-color);
52+
cursor: pointer;
53+
display: flex;
54+
align-items: center;
55+
font-size: var(--font-size-primary);
56+
overflow: hidden;
57+
`;
58+
59+
const FilterIcon = styled.div`
60+
margin-left: 0.375rem;
61+
display: flex;
62+
align-items: center;
63+
`;
64+
65+
const SelectedValue = styled.div`
66+
font-weight: bold;
67+
margin-left: 0.25rem;
68+
overflow: hidden;
69+
text-overflow: ellipsis;
70+
min-width: 0;
71+
`;
72+
73+
export default Filter;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React, { useEffect, useRef } from 'react';
2+
import styled from 'styled-components';
3+
import { PopoverStyles } from '../Popover';
4+
5+
const FilterPopup: React.FC<{ children: React.ReactNode; onClose: () => void }> = ({ children, onClose }) => {
6+
const ref = useRef<HTMLDivElement>(null);
7+
8+
useEffect(() => {
9+
function handleClickOutside(event: MouseEvent) {
10+
if (ref.current && event.target instanceof Node && !ref.current.contains(event.target)) {
11+
onClose();
12+
}
13+
}
14+
document.addEventListener('mousedown', handleClickOutside);
15+
return () => {
16+
document.removeEventListener('mousedown', handleClickOutside);
17+
};
18+
}, [ref, onClose]);
19+
20+
return <FilterPopupBase ref={ref}>{children}</FilterPopupBase>;
21+
};
22+
23+
const FilterPopupBase = styled.div`
24+
${PopoverStyles}
25+
position: absolute;
26+
top: 100%;
27+
margin-top: 0.375rem;
28+
left: 0;
29+
z-index: 999;
30+
min-width: 12.5rem;
31+
max-width: 18.75rem;
32+
`;
33+
34+
export default FilterPopup;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'react';
2+
import styled from 'styled-components';
3+
import { CheckboxField } from '@components/Form/Checkbox';
4+
import Icon from '@components/Icon';
5+
6+
export const FilterOptionRow: React.FC<{ onClick: () => void; selected: boolean; children: React.ReactNode }> = ({
7+
children,
8+
selected = false,
9+
onClick,
10+
}) => (
11+
<FilterClickableRow onClick={onClick}>
12+
<CheckboxField checked={selected} />
13+
{children}
14+
</FilterClickableRow>
15+
);
16+
17+
export const FilterClickableRow = styled.div<{ disabled?: boolean; danger?: boolean }>`
18+
display: flex;
19+
align-items: center;
20+
gap: 0.5rem;
21+
22+
border-radius: var(--radius-primary);
23+
padding: 0.375rem 0.5rem;
24+
cursor: pointer;
25+
26+
opacity: ${(p) => (p.disabled ? 0.2 : 1)};
27+
color: ${(p) => (p.danger ? 'var(--color-text-danger)' : 'var(--color-text-primary)')};
28+
29+
&:hover {
30+
background: var(--color-bg-secondary);
31+
}
32+
`;
33+
34+
type ClearFilterRowProps = {
35+
onClick: () => void;
36+
disabled: boolean;
37+
};
38+
export const ClearFilterRow: React.FC<ClearFilterRowProps> = ({ onClick, disabled }) => (
39+
<FilterClickableRow onClick={onClick} danger disabled={disabled}>
40+
<Icon name="trash" /> Clear filter
41+
</FilterClickableRow>
42+
);
43+
44+
export const FilterSeparator = styled.div`
45+
border-top: var(--border-thin-1);
46+
margin: 0.5rem 0;
47+
`;
48+
49+
export const FilterPopupTrailing: React.FC<{ clear?: ClearFilterRowProps; children?: React.ReactNode }> = ({
50+
children,
51+
clear,
52+
}) => (
53+
<div>
54+
<FilterSeparator />
55+
{clear && <ClearFilterRow {...clear} />}
56+
{children}
57+
</div>
58+
);

src/components/Form/Checkbox.tsx

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Icon from '@components/Icon';
88
//
99

1010
type CheckboxFieldProps = {
11-
label: string;
11+
label?: string | null;
1212
checked: boolean;
1313
className?: string;
1414
onChange?: () => void;
@@ -32,6 +32,7 @@ export const CheckboxField: React.FC<CheckboxFieldProps> = ({
3232
}) => {
3333
const [id] = useState(uuid());
3434
const testid = rest['data-testid'];
35+
3536
return (
3637
<CheckboxWrapper
3738
checked={checked}
@@ -41,7 +42,7 @@ export const CheckboxField: React.FC<CheckboxFieldProps> = ({
4142
disabled={disabled}
4243
>
4344
<span className={`checkbox ${id}`}>{checked && <Icon name="check" />}</span>
44-
<label htmlFor={id}>{label}</label>
45+
{label && <label htmlFor={id}>{label}</label>}
4546
</CheckboxWrapper>
4647
);
4748
};
@@ -66,42 +67,46 @@ const CheckboxWrapper = styled.div<{ checked: boolean; disabled: boolean }>`
6667
}
6768
6869
span.checkbox {
69-
color: #fff;
70-
width: 1.25rem;
71-
height: 1.25rem;
72-
line-height: 1.25rem;
73-
display: inline-block;
74-
border-radius: 0.125rem;
70+
display: flex;
71+
align-items: center;
72+
justify-content: center;
73+
width: var(--checkbox-size);
74+
height: var(--checkbox-size);
75+
line-height: var(--checkbox-size);
76+
border-radius: var(--checkbox-border-radius);
7577
text-align: center;
76-
background: var(--color-bg-secondary);
77-
border: var(--border-primary-thin);
78+
background: var(--checkbox-background);
79+
border: var(--checkbox-border);
7880
}
7981
8082
span.checkbox svg path {
8183
fill: transparent;
8284
}
8385
86+
svg {
87+
max-width: 100%;
88+
}
89+
8490
${(p) =>
8591
p.checked &&
8692
css`
8793
span.checkbox {
88-
color: var(--color-text-alternative);
89-
border-color: var(--color-bg-brand-primary);
90-
color: var(--color-bg-brand-primary);
91-
background: var(--color-bg-primary);
94+
border-color: var(--checkbox-color-checked);
95+
color: var(--checkbox-color-checked);
96+
background: var(--checkbox-background-checked);
9297
}
9398
9499
&.status-running span.checkbox {
95-
border-color: var(--color-bg-success-light);
96-
color: var(--color-bg-success-light);
100+
border-color: var(--checkbox-color-checked-success-light);
101+
color: var(--checkbox-color-checked-success-light);
97102
}
98103
&.status-failed span.checkbox {
99-
border-color: var(--color-bg-danger);
100-
color: var(--color-bg-danger);
104+
border-color: var(--checkbox-color-checked-danger);
105+
color: var(--checkbox-color-checked-danger);
101106
}
102107
&.status-completed span.checkbox {
103-
border-color: var(--color-bg-success);
104-
color: var(--color-bg-success);
108+
border-color: var(--checkbox-color-checked-success);
109+
color: var(--checkbox-color-checked-success);
105110
}
106111
`}
107112
`;

0 commit comments

Comments
 (0)