Skip to content

Commit

Permalink
URL-persist changeset filters (#12662)
Browse files Browse the repository at this point in the history
This also refactors the filters into a separate component, which will make it easier to implement the new UI components based on this.
  • Loading branch information
eseliger committed Aug 4, 2020
1 parent 77e8670 commit 2cc3071
Show file tree
Hide file tree
Showing 4 changed files with 601 additions and 199 deletions.
Expand Up @@ -3,14 +3,14 @@ import { CampaignChangesets } from './CampaignChangesets'
import * as H from 'history'
import { of, Subject } from 'rxjs'
import { NOOP_TELEMETRY_SERVICE } from '../../../../../../shared/src/telemetry/telemetryService'
import { shallow } from 'enzyme'
import { mount } from 'enzyme'
import { ChangesetExternalState, ChangesetState } from '../../../../graphql-operations'

describe('CampaignChangesets', () => {
const history = H.createMemoryHistory()
test('renders', () =>
expect(
shallow(
mount(
<CampaignChangesets
queryChangesets={() =>
of({
Expand Down
212 changes: 138 additions & 74 deletions web/src/enterprise/campaigns/detail/changesets/CampaignChangesets.tsx
Expand Up @@ -29,6 +29,7 @@ import { TelemetryProps } from '../../../../../../shared/src/telemetry/telemetry
import { property, isDefined } from '../../../../../../shared/src/util/types'
import { useObservable } from '../../../../../../shared/src/util/useObservable'
import { ChangesetFields } from '../../../../graphql-operations'
import { isValidChangesetExternalState, isValidChangesetReviewState, isValidChangesetCheckState } from '../../utils'

interface Props extends ThemeProps, PlatformContextProps, TelemetryProps, ExtensionsControllerProps {
campaign: Pick<GQL.ICampaign, 'id' | 'closedAt' | 'viewerCanAdminister'>
Expand All @@ -41,17 +42,10 @@ interface Props extends ThemeProps, PlatformContextProps, TelemetryProps, Extens
queryChangesets?: typeof _queryChangesets
}

function getLSPTextDocumentPositionParameters(
hoveredToken: HoveredToken & RepoSpec & RevisionSpec & FileSpec & ResolvedRevisionSpec
): RepoSpec & RevisionSpec & ResolvedRevisionSpec & FileSpec & UIPositionSpec & ModeSpec {
return {
repoName: hoveredToken.repoName,
revision: hoveredToken.revision,
filePath: hoveredToken.filePath,
commitID: hoveredToken.commitID,
position: hoveredToken,
mode: getModeFromPath(hoveredToken.filePath || ''),
}
interface ChangesetFilters {
externalState: GQL.ChangesetExternalState | null
reviewState: GQL.ChangesetReviewState | null
checkState: GQL.ChangesetCheckState | null
}

/**
Expand All @@ -69,24 +63,23 @@ export const CampaignChangesets: React.FunctionComponent<Props> = ({
telemetryService,
queryChangesets = _queryChangesets,
}) => {
const [externalState, setExternalState] = useState<GQL.ChangesetExternalState | undefined>()
const [reviewState, setReviewState] = useState<GQL.ChangesetReviewState | undefined>()
const [checkState, setCheckState] = useState<GQL.ChangesetCheckState | undefined>()

const [changesetFilters, setChangesetFilters] = useState<ChangesetFilters>({
checkState: null,
externalState: null,
reviewState: null,
})
const queryChangesetsConnection = useCallback(
(args: FilteredConnectionQueryArgs) =>
merge(of(undefined), changesetUpdates).pipe(
switchMap(() =>
queryChangesets({
...changesetFilters,
first: args.first ?? null,
campaign: campaign.id,
externalState: externalState ?? null,
reviewState: reviewState ?? null,
checkState: checkState ?? null,
}).pipe(repeatWhen(notifier => notifier.pipe(delay(5000))))
)
),
[campaign.id, externalState, reviewState, checkState, queryChangesets, changesetUpdates]
[campaign.id, changesetFilters, queryChangesets, changesetUpdates]
)

const containerElements = useMemo(() => new Subject<HTMLElement | null>(), [])
Expand Down Expand Up @@ -143,62 +136,9 @@ export const CampaignChangesets: React.FunctionComponent<Props> = ({
componentRerenders.next()
}, [componentRerenders, hoverState])

const changesetFiltersRow = (
<div className="form-inline mb-0 mt-2">
<label htmlFor="changeset-state-filter">State</label>
<select
className="form-control mx-2"
value={externalState}
onChange={event =>
setExternalState((event.target.value || undefined) as GQL.ChangesetExternalState | undefined)
}
id="changeset-state-filter"
>
<option value="">All</option>
{Object.values(GQL.ChangesetExternalState).map(state => (
<option value={state} key={state}>
{upperFirst(lowerCase(state))}
</option>
))}
</select>
<label htmlFor="changeset-review-state-filter">Review state</label>
<select
className="form-control mx-2"
value={reviewState}
onChange={event =>
setReviewState((event.target.value || undefined) as GQL.ChangesetReviewState | undefined)
}
id="changeset-review-state-filter"
>
<option value="">All</option>
{Object.values(GQL.ChangesetReviewState).map(state => (
<option value={state} key={state}>
{upperFirst(lowerCase(state))}
</option>
))}
</select>
<label htmlFor="changeset-check-state-filter">Check state</label>
<select
className="form-control mx-2"
value={checkState}
onChange={event =>
setCheckState((event.target.value || undefined) as GQL.ChangesetCheckState | undefined)
}
id="changeset-check-state-filter"
>
<option value="">All</option>
{Object.values(GQL.ChangesetCheckState).map(state => (
<option value={state} key={state}>
{upperFirst(lowerCase(state))}
</option>
))}
</select>
</div>
)

return (
<>
{changesetFiltersRow}
<ChangesetFilterRow history={history} location={location} onFiltersChange={setChangesetFilters} />
<div className="list-group position-relative" ref={nextContainerElement}>
<FilteredConnection<ChangesetFields, Omit<ChangesetNodeProps, 'node'>>
className="mt-2"
Expand All @@ -218,7 +158,7 @@ export const CampaignChangesets: React.FunctionComponent<Props> = ({
pluralNoun="changesets"
history={history}
location={location}
useURLQuery={false}
useURLQuery={true}
/>
{hoverState?.hoverOverlayProps && (
<WebHoverOverlay
Expand All @@ -236,3 +176,127 @@ export const CampaignChangesets: React.FunctionComponent<Props> = ({
</>
)
}

interface ChangesetFilterRowProps {
history: H.History
location: H.Location
onFiltersChange: (newFilters: ChangesetFilters) => void
}

const ChangesetFilterRow: React.FunctionComponent<ChangesetFilterRowProps> = ({
history,
location,
onFiltersChange,
}) => {
const searchParameters = new URLSearchParams(location.search)
const [externalState, setExternalState] = useState<GQL.ChangesetExternalState | undefined>(() => {
const value = searchParameters.get('external_state')
return value && isValidChangesetExternalState(value) ? value : undefined
})
const [reviewState, setReviewState] = useState<GQL.ChangesetReviewState | undefined>(() => {
const value = searchParameters.get('review_state')
return value && isValidChangesetReviewState(value) ? value : undefined
})
const [checkState, setCheckState] = useState<GQL.ChangesetCheckState | undefined>(() => {
const value = searchParameters.get('check_state')
return value && isValidChangesetCheckState(value) ? value : undefined
})
useEffect(() => {
const searchParameters = new URLSearchParams(location.search)
if (externalState) {
searchParameters.set('external_state', externalState)
} else {
searchParameters.delete('external_state')
}
if (reviewState) {
searchParameters.set('review_state', reviewState)
} else {
searchParameters.delete('review_state')
}
if (checkState) {
searchParameters.set('check_state', checkState)
} else {
searchParameters.delete('check_state')
}
history.replace({ ...location, search: searchParameters.toString() })
// Update the filters in the parent component.
onFiltersChange({
externalState: externalState || null,
reviewState: reviewState || null,
checkState: checkState || null,
})
// We cannot depend on the history, since it's modified by this hook and that would cause an infinite render loop.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [externalState, reviewState, checkState])
return (
<div className="form-inline mb-0 mt-2">
<ChangesetFilter<GQL.ChangesetExternalState>
values={Object.values(GQL.ChangesetExternalState)}
label="State"
htmlID="changeset-state-filter"
selected={externalState}
onChange={setExternalState}
/>
<ChangesetFilter<GQL.ChangesetReviewState>
values={Object.values(GQL.ChangesetReviewState)}
label="Review state"
htmlID="changeset-review-state-filter"
selected={reviewState}
onChange={setReviewState}
/>
<ChangesetFilter<GQL.ChangesetCheckState>
values={Object.values(GQL.ChangesetCheckState)}
label="Check state"
htmlID="changeset-check-state-filter"
selected={checkState}
onChange={setCheckState}
/>
</div>
)
}

interface ChangesetFilterProps<T extends string> {
label: string
htmlID: string
values: T[]
selected: T | undefined
onChange: (value: T | undefined) => void
}

export const ChangesetFilter = <T extends string>({
htmlID,
label,
values,
selected,
onChange,
}: ChangesetFilterProps<T>): React.ReactElement<ChangesetFilterProps<T>> => (
<>
<label htmlFor={htmlID}>{label}</label>
<select
className="form-control mx-2"
value={selected}
onChange={event => onChange((event.target.value ?? undefined) as T | undefined)}
id={htmlID}
>
<option value="">All</option>
{values.map(state => (
<option value={state} key={state}>
{upperFirst(lowerCase(state))}
</option>
))}
</select>
</>
)

function getLSPTextDocumentPositionParameters(
hoveredToken: HoveredToken & RepoSpec & RevisionSpec & FileSpec & ResolvedRevisionSpec
): RepoSpec & RevisionSpec & ResolvedRevisionSpec & FileSpec & UIPositionSpec & ModeSpec {
return {
repoName: hoveredToken.repoName,
revision: hoveredToken.revision,
filePath: hoveredToken.filePath,
commitID: hoveredToken.commitID,
position: hoveredToken,
mode: getModeFromPath(hoveredToken.filePath || ''),
}
}

0 comments on commit 2cc3071

Please sign in to comment.